REVERB A Findaway Technology Blog Thu, 01 May 2014 11:48:48 +0000 en-US hourly 1 http://wordpress.org/?v=3.7.5 Bookopotamus: A Game Analytics Case Study /development/bookopotamus-a-game-analytics-case-study/ /development/bookopotamus-a-game-analytics-case-study/#comments Thu, 01 May 2014 11:47:43 +0000 /?p=1418 In December we released Bookopotamus for iOS and Android, a fun literary guessing game that uses Findaway World’s catalog of audiobooks. It plays a narrated quote from a book, and you see how fast you can identify the book you’re listening to.

I’m a big fan of using data to inform decisions, so I volunteered to take charge of the analytics for Bookopotamus. I’ve integrated analytics into projects before, but never for a game. This was an exciting opportunity to capture some really actionable data that could help us make the game even more engaging and fun.

The Platform

The Bookopotamus analytics are built from the ground up with Keen IO. We chose to use Keen IO for two reasons. First, we were about to start using Keen IO on a much larger project, and I knew that playing around with the Bookopotamus integration would give me a great opportunity to understand it inside and out before going all-in on a much higher-stakes project. Second, I wanted to stay focused on the data that really mattered and keep the analytics footprint small. It’s much easier to capture custom events with Keen IO than Google Analytics, and far easier to analyze the resulting data. Just throwing Google Analytics in the app would have been easier, but I wanted to take a more deliberate approach to analytics, without the data bloat of Google Analytics. Why? Because data is worthless if nobody uses it.

The Data

I ran into a few surprises with data points I was used to getting for free with Google Analytics that I had to build up myself. Nothing was particularly difficult or complicated, but I’ve documented how we approached capturing device and operating statistics, along with anonymized user and session data. Beyond that, I’ll show some other custom events that we’re capturing. We used the same approaches for both iOS and Android, but I’m just going to use code samples from our iOS integration in the examples below. For the analysis/query parameters, I’m following the format that Keen uses in their simple “Workbench” area that looks like this:

Screenshot of Keen's Workbench

Devices & OS Versions

Simply put, we wanted to know what people were using to play the game. Are they on a phone or a tablet, iOS or Android? Download numbers from the app stores tell one story, but they can’t show engagement. We wanted to be able to break down the number of games played, average scores, and how many unique users we have by device and platform.

Capturing

To capture this, I set up a few global properties to be sent with every event. I structured this information in a nested JSON object named device. device has a property for model, which is [[UIDevice currentDevice] model] (example: “iPhone” or “iPad”). There’s also an os property that has two nested properties: name which is just a static string “iOS”, and version which is [[UIDevice currentDevice] systemVersion] (looks like “7.0.1″).

Nesting the properties like this is just for convenience later. It keeps the names short and descriptive in the code, but Keen will group them together in the reporting dropdowns, using dot notation for any nested properties. This means that while I use “version” in the code, it’ll show up as device.os.version in Keen.

Analyzing

I tend to think of global properties like these as auxiliary metrics. Most of them aren’t really useful by themselves, but they become very powerful when paired with other metrics. When analyzing data, you will usually use your global properties to group the results of other metrics. For example, if we wanted to see who was getting better scores, iOS or Android users, we could construct the following query:

  • Event Collection: gameplay
  • Type: Average
  • Target Property: game.score
  • Group By: device.os.name
  • Filter: ‘event’ equals ‘game_finished’

Keen will report this in one of two ways. If there’s no time breakdown, you’ll see a pie chart. If there is a time breakdown, you’d see a line chart with two lines bouncing up and down, showing the average scores per interval (i.e. day) for each of the platforms.

It’s worth noting the addition of the filter in this query. Since we record a game.score value for multiple events (when a game finishes, and again as a separate event if it’s a new high score), it’s important to focus in on the relevant event, otherwise the report would be skewed higher since everyone’s high scores would be reported twice.

Users and Sessions

Another important auxiliary metric is around your users, and their ‘sessions’, or how many distinct times they’re interacting with your app. This was another freebie in Google Analytics that we had to construct ourselves. The good news is that it was simple to do, even though we don’t have any notion of user accounts in the game. There’s no login in Bookopotamus; your score data and progress is all stored directly on the device.

Our  motivation for wanting to capture info about users and sessions was that we wanted to know how many unique people were playing the game, and how often they were playing it. We want to understand how many times people play before they stop. If we add more questions later on, will they start playing again? When does the game get boring? Since all the user data is anonymized, we could hypothetically follow the trail of an individual user, but it would never tie back to someone specific.

Capturing

The first time the app is opened, we generate a UUID and store it in NSUserDefaults. We want this to stay the same for the life of the app, but be unique to this user, so the code below only generates the UUID if it doesn’t exist (app’s first launch).

Once this is set in the NSUserDefaults, we add it to our global properties so that the user_id is sent with every event.

We do something similar for sessions. We consider a session to end when Bookopotamus enters the background, so every time the applicationDidBecomeActive is invoked, we set a new UUID to the session_id key in the NSUserDefaults. Like the user_id, I add this to the global event properties so it gets sent with every event.

This way of tracking sessions isn’t foolproof, but it’s good enough to work for us. If you wanted to make this a little more robust, you could set an amount of time that needs to elapse between applicationDidEnterBackground and applicationDidBecomeActive for the app to consider it a new session. This would eliminate the duplicate session if they pop out of the app momentarily to take a phone call or check a Facebook notification.

When we’re done, the relevant block in our global properties dictionary looks like this:

Analyzing

Even though these are global properties like the device metrics, we do often analyze the the user and session data as a primary metric. This is the exception to the rule because the values of each of these properties are unique, and can’t be grouped.

Let’s say we want to find out how many different people have opened our app (not the same as a raw download count!). We can construct a simple query that looks like this:

  • Event Collection: gameplay
  • Analysis Type: count_unique
  • Target Property: user.anon_id

This will count up the total number of unique anon_id properties in any of the events in the ‘gameplay’ collection. I chose this collection since we record a ‘new_session’ event on every app launch in this collection, so this will report the total number of users who have opened the app, but not necessarily finished a game. If we wanted to see how many people finished a game, we could add a filter like this:

  • Event Collection: gameplay
  • Analysis Type: count_unique
  • Target Property: user.anon_id
  • Filter: ‘event’ equals ‘game_finished’

Custom Gameplay Events

There are a few other events that are very specific to the gameplay of Bookopotamus that we wanted to track. These events don’t live in the global properties, but instead are dedicated events that get sent to Keen IO

New Sessions

Every time the app is launched, or the applicationDidBecomeActive method is triggered, we set a new session identifier in the NSUserDefaults (covered above), and send a dedicated event to the ‘gameplay’ collection that looks like this:

We’re capturing this metric because we want to see how many sessions per user we have. This will help us get a feel for engagement. How many people come back to the game? To find out how many sessions we’ve had, we build a query that looks like this:

  • Event Collection: gameplay
  • Analysis Type: count_unique
  • Target Property: user.session_id

To find out how many sessions/user, we can just divide the number of unique users by the number of unique sessions. It’s important to note that if we were to actually count the raw number of user.session_ids instead of counting the *unique* number of session_ids, we’d get a drastically higher number because the user.session_id is a global property that gets sent back with every event. By adding a timeframe and interval, we can see a line graph that can give us one perspective of popularity over time.

Finished Games

Whenever you finish a game, we also capture some data around how you did. Here’s what it looks like:

This gives us a basic view of how games are going, and how many games are being played. One way we’re using this is to find out how many people who open the app end up playing a game. We can find out by counting the number of unique user.anon_ids and adding a filter so that we’re only counting while scoped to ‘game_finished’ events like this:

  • Event Collection: gameplay
  • Analysis Type: count_unique
  • Target Property: anon_id
  • Filter: ‘event’ equals ‘game_finished’

We can find out who gets better scores, iOS or Android users (dangerous territory here) by grouping the results of a score average by device.os.name like this:

  • Event Collection: gameplay
  • Analysis Type: average
  • Target Property: score
  • Group By: device.os.name
  • Filter: ‘event’ equals ‘game_finished’

We expect this number to be very near equal, but it could give us an indication that something is wrong if they differ significantly. Worth noting is the filter at the bottom that’s limiting results to ‘game_finished’ events. This is again important because the score property is also used for a custom event capturing high scores that is also in the gameplay collection. We’d get a slightly higher average if we didn’t limit to ‘game_finished’ events because every high score would get reported twice.

High Scores

If you get a high score, we’re recording it like this:

This event only gets reported when you get a new high score, but it doesn’t take the place of the ‘game_finished’ event. We want to know how often people get a new top score, which is another measure of engagement. Are they striving to get better? Do they get a top score right away out of luck and then never top it? Another suspicion we had was that people were more likely to share their score on Facebook or Twitter when it was a high score. Let’s run through how we can find this out.

Social Sharing

At the end of a game, a screen shows up with your score, and an option to Tweet or share your score on Facebook. We’re capturing events when either one of those happens:

The Facebook event looks exactly the same, except the event name is “score_facebooked” instead. With this data captured, we can find out whether a high score affects someone’s likelihood of sharing.

  • Event Collection: gameplay
  • Analysis Type: count
  • Target Property: keen.id
  • Group By: isHighScore
  • Filter: ‘event’ equals ‘score_facebooked’

This query is counting the number of keen.id properties (generated by Keen and unique with every send, great for counting up events) and grouping them by whether or not they are a high score. The filter is limiting data to only ‘score_facebooked’ events. As of now, we can see that the breakdown is just about even. For Twitter, we’re seeing a split of 48% of tweets were also a high score for someone, whereas 52% of tweets were not their high score. For Facebook, the split is 53% were high scores, 47% were not. It would appear as if you are no more likely to share a high score than any other score.

Individual Question Data

Possibly the most interesting piece of data we’re collecting is focused around the audio clips themselves. Up till now, every example has been limited to the ‘gameplay’ collection. We only have one other collection, and it’s only capturing one event right now: an ‘answer.’ Our ‘questions’ collection captures data about the performance of each audio clip. The captured data format looks like this:

The best data we’ve gotten out of this is about the difficulty of each question. A simple query like this can easily show us what are hardest, and easiest questions are:

  • Event Collection: questions
  • Analysis Type: average
  • Target Property: answer.score
  • Group By: answer.correct_text

This gives us a nice output of each title’s average score. For example, about 6 months of data tells us that “A Room with a View” gets an average score of about 39, and “A Tale of Two Cities” gets an average score of about 142. Next, we can start using that data to find out if people play more often if they get high scores (feel smart), or if they’re more challenged by the questions. We can use that data to inform the next round of clips that get loaded into Bookopotamus.

Conclusion

Well, that’s the data we’re collecting in Bookopotamus, and how we’re using it. We focused on actionable data that can influence changes, new features, and content updates. I’m looking forward to a followup post alongside the next Bookopotamus update that can show and explain decisions we’ve made along with the data that drove those decisions, and another followup post after that showing how we measured and reacted to the results. In the meantime, enjoy a few rounds of Bookopotamus.

]]>
/development/bookopotamus-a-game-analytics-case-study/feed/ 0
Cache-busting HTML Pages /problem-solved/cache-busting-html-pages/ /problem-solved/cache-busting-html-pages/#comments Mon, 17 Feb 2014 18:23:01 +0000 /?p=1461 I ran into a scenario twice this week where I pushed updates to a site and it was important that the changes were reflected immediately. Most of the time it isn’t a big deal if a change takes an hour or so before someone sees them without a forced refresh (Shift + Refresh). Indeed, according to the Apache Caching Guide, “The default expiry period for cached entities is one hour.” This time, I was making a change, and then sending out an email to all our subscribers linking directly to the change. I only got the go-ahead to push the change live about 5 minutes before the email was supposed to be sent. On a final test of the email blast, I noticed the link went to the old version; the changes weren’t being shown until after I refreshed a few times.

This presented 2 problems:

  1. I know how to cache-bust CSS and Javascript resources by appending a query string to their path (like main.css?v=2), but that didn’t do me any good if a stale version of the HTML was being served.
  2. I only had a limited number of tests on machines in the building to determine whether my fix worked. As soon as the browser loads the new version of the site, there’s no longer any way to simulate the problem with that Browser + Machine combination without reverting changes on the server, which wasn’t an option.

Nothing useful showed up for my initial search: ‘Cache bust HTML’ (hence the title of this post… take note, Google). After a little more digging a colleague and I came up with the solution of sending back a Cache-Control header that forced resources matching *.html to expire after 1 minute. This worked! Here’s how I did it.

The Solution

Start by SSHing into your server and making sure you have the `headers` apache module enabled. You can check in the `mods_enabled` folder in your apache config (ours is at /etc/apache2/mods_enabled on our Ubuntu box). If it isn’t enabled, run `a2enmod headers` and follow the instructions to restart apache. If you’re on a shared hosting plan, you may not be able to check or change these kinds of settings. You can still try the code below, which will fail gracefully since it’s wrapped in the IFModule condition.

Next, add the following to your .htaccess file:

Change the max-age parameter to whatever is reasonable for you. The max-age is in seconds, so I have it set for one minute above. Once you apply this, it should take effect immediately. To verify, open up the Network tab in your developer tools, refresh, and inspect the response headers for the page you’re targeting. You should see the Cache-Control header with the max-age that you set in your .htaccess!

The next morning, I changed this to 3600, or 1 hour. There’s no need to always set the max-age to 1 minute, but I left the whole block in so I could quickly update the number in case I need to use this again.

It’s worth noting there are plenty of ways to set headers depending on your project server configuration. This was for a static site served by apache, so using .htaccess made sense for us. There are other ways to do this, research the best way to set headers for your project and focus on the Cache-Control header above.

]]>
/problem-solved/cache-busting-html-pages/feed/ 0
Using Bower + Grunt in existing python projects /development/using-bower-grunt-in-existing-python-projects/ /development/using-bower-grunt-in-existing-python-projects/#comments Tue, 07 Jan 2014 13:20:11 +0000 /?p=1435 When I set out to integrate Bower and Grunt into our current Python projects I wasn’t looking to upset the balance, but instead improve the process for front-end development. Integrating these tools allows us to lighten our repository by moving third party dependencies to Bower, along with giving us easier access to build tools like Require.js and Sass. Ultimately these tools give us the ability to deploy optimized assets without the need to check them into git and manage changes to built files across branches.

File Structure and setup

With our current python file structure, we have a nice setup for integrating the new tools. There are a number of files that need to be added to utilize the new tools.

Bower

Setting up Bower is simple.

$ bower init

Answer the prompts to generate your bower.json.

After you have a bower.json you will need to create a .bowerrc to tell Bower where to install components.

In your favorite editor open .bowerrc and add the following.


This tells Bower to install its components into a bower_components folder under the subdirectory AppName. If you didn’t do this, your site would not have access to the installed components because they are outside the web server root.

Now you are free to install components.

$ bower install --save jquery

Grunt

Once you have bower you can start installing components, but you need to be able to build them. Enter Grunt.

Grunt is a task runner written in Javascript for Node.js. Grunt will allow us to write some tasks to do things like compile javascript with r.js, compile Sass with node-sass, start the development server, and actively watch files for changes.

We first need to create a package.json to outline what node packages we need.

We will be using a few grunt plugins to handle creating tasks for us.

  • node-sass – Implements Node bindings for libsass.
  • grunt-contrib-watch – tasks for watching files and executing tasks when changes occur.
  • grunt-sass – tasks for building Sass with node-sass.
  • grunt-contrib-requirejs – tasks for compiling javascript with requirejs and r.js.
  • load-grunt-tasks – automatically load grunt tasks from installed plugins.
  • time-grunt – display execution summary on end.

Once we have our package.json created and dependencies declared we can install those dependancies with one command.

$ npm install

After our dependancies are installed we can write our Gruntfile.

While there is a lot going on in this Gruntfile.js, there are a few key items to explain.

Running the server and building sass

We have a custom Grunt task called ‘runApp’ that will kick off our python server using the ‘run_appname.sh’ bash script.

The task starts by spawning a child process using Node’s built in child process tasks, and runs the ‘run_appname.sh’ script. We don’t need to pass any arguments to the script, but we do set the cwd (current working directory) with the options argument. Additionally we log out all events from the child process, stdout, stderr, data, and close.

To run the server in tandem with the watch task we have a combined task called ‘server’ that will first start the python server and second start the watch to rebuild JS and CSS files when they change.

Building and deploying

Since we are using javascript and Sass compilers there will inevitably come a time when we need to build those for deployment. With Grunt we have a task to do just that.

$ grunt build

This task is almost never necessary for development – ‘watch’ and ‘server’ should be used then. The ‘build’ task is mostly intended to be used on deployment. After the latest has been pulled from git ‘$ grunt build’ should be run to compile the latest css and javascript from source. This allows us to not only deliver small/optimized files, but now there is no need to commit compiled files to source control.

While this simplifies things a little it also adds some overhead to our server stack. In addition to needing Python we also need Node and NPM.

The typical deployment workflow would be as follows.

  1. fetch the latest from origin
  2. pull the latest code from the master branch
  3. activate the virtualenv and update/install any python packages
  4. install or update node packages
  5. install or update bower components
  6. build the latest css and js using grunt
  7. restart the service/application using supervisord

This is a simple deploy workflow, there could be more complex steps involved like tagging new and old releases, etc…

Python Configuration

Using Require.js and r.js to build our javascript for production means we will have two different files for development and production. On build r.js creates ‘main-built.js’ for production, we need a way to tell our views what environment version to use.

To configure the views to use either development or production versions of our files we use a ‘Config’ python class. Imported from ‘/AppName/common/config.py’ as ‘CONFIG’.

This sets up our configuration variables, of which we have a ‘js’ object.

The ‘CONFIG.js’ object contains two properties.

  • ‘main’ – this is the name of the main javascript file (main for un-built, and main-built for built)
  • ‘loader’ – the loader script to use (require.js, almond.js, etc AMD loader…)

The config.py values can be overridden for local development via ‘/AppName/common/local_config.py’.

The local config is never checked into version control, allowing everyone to configure the app for their environment without conflict.

View setup

In our route callback we return the ‘CONFIG.js’ data to the views as the ‘js’ property, along with any other view data.

In our view we output the correct loader and main file using the ‘js.loader’ and ‘js.main’ properties respectively.

Final thoughts

As we begin to use these tools on more projects I suspect more changes/improvements will happen. This first attempt was to simply integrate tools we are accustomed to using on other simpler projects into more advanced projects that utilize server languages. As we improve this integration I would like to see us attempt to follow this process with other languages like PHP, and frameworks like Magento.

]]>
/development/using-bower-grunt-in-existing-python-projects/feed/ 0
CSS background-size and background-position /web/css-background-size-and-background-position/ /web/css-background-size-and-background-position/#comments Wed, 18 Dec 2013 12:32:16 +0000 /?p=1429 It feels like I come back to this awesome Pen at least once a month, so I wanted to share. It’s a great way to see how the different properties for background-size and background-position affect the image. At first, I went here to figure out which option it was that I wanted, now I use it to quickly illustrate the differences to others. Enjoy!

http://codepen.io/herihehe/pen/aLwGt

]]>
/web/css-background-size-and-background-position/feed/ 0
Default javascript object sorting across browsers /development/javascript-object-sorting/ /development/javascript-object-sorting/#comments Fri, 01 Nov 2013 14:42:27 +0000 /?p=1391 Today I was presented with puzzling issue that I had never encountered before. When accessing an item from a javascript object using underscore’s _.first() method I was getting two different items in Chrome and Firefox.

My object is an object with a string keys. The key is a numeric string (’01234′ ’12345′ etc) because javascript will trim off the leading zero if converted to a number.

Lets say in this example the order is correct. In Chrome this object when accessed will order 01234 after 23456 and firefox will order it before 12345. The only way to force an order of items is to convert the keys to numbers and it will be sorted by key numerically (dropping the leading 0 on 01234).

Interestingly enough I found that if the keys were alpha strings Firefox sorted them alphabetically and Chrome did not.

In this example chrome would respect the order of items in the object but Firefox would order them ‘a’, ‘b’, ‘c’.

This seriously calls into question why browsers are applying a sort to any object. The JSON spec does not dictate a default order there for browsers should not assume or try to assume any order other than the natural order of items.

]]>
/development/javascript-object-sorting/feed/ 0
human.conf /musings/human-conf/ /musings/human-conf/#comments Thu, 20 Jun 2013 14:24:26 +0000 /?p=866 Recently, a co-worker commented that it would be nice if I had a configuration file. This individual suggested a change to my configuration file that would result in more detailed output when questioned about project status would be nice.  An idea that sounded really intriguing! Fortunately for me, I’m no robot. I was configured by my parents years ago. That said, here is my current configuration file…


#
# Aurelius.conf
#
# Home
STATUS=Married
NUM_CHILDREN=1.5
HAIR=Blonde
EYES=Blue
SHOE_SIZE=9
HOBBIES=Mountain Biking, Golf, Video Games
FAVORITE_FOOD=Mexican
FAVORITE_MOVIE=Hudson Hawk
# Work
TITLE=Product Developer
PREFERRED_LANGUAGE=Java
PROJECT_STATUS_DETAIL=2

]]>
/musings/human-conf/feed/ 0
Edges, Corners, and Malformed Pickles /development/edges-corners-and-malformed-pickles/ /development/edges-corners-and-malformed-pickles/#comments Tue, 28 May 2013 19:38:49 +0000 /?p=1339 Pickle

A recent blog post here mentioned the difficulty that edge cases present. While we sometimes use ‘edge case’ to mean ‘rare condition’; if you want to be technical (and I do) specifically it refers to a case that occurs at an extreme of an operating parameter (a stereo speaker that fails at maximum volume).

In Jason’s example, the ‘edge’ is on the minimum side; if there’s no record in one system, the process stopped even though the other system could possibly have records in it. By analogy a ‘corner case’ is when more than one parameter is near it’s ‘edge’; two edges meet, and make a corner. (Say, when that same speaker only fails at maximum volume AND when in an environment of high humidity.)

Of course, in common parlance, we’ll use both terms to just mean a ‘rare case’. But of course, as a system sees more and more use, a rare case can become more and more common. Tonight, for your entertainment, two such strange cases. Normally, at the end of these technical posts there will be some little lesson or insight to take away; hopefully a simple trick or rule to follow to avoid a pitfall or pratfall. In this case, there won’t be one, really. Sometimes things are tricky; to find out what’s wrong you need to check the logs, read and re-read the code, think about the process, check a hunch, follow your gut, try a fix, fail, and start over again. A few times.

The Tale of the Vanishing Remainder

Submitted for your approval. One Ms. Crypto; a simple backend program that, together with its partner (Mr. Crypto) works to keep files encrypted and safe. Yet, some of the files are not making it to their intended destinations on mobile devices. The device simply sits and waits… for a packet that never comes. Lost in the ether? Blocked by a firewall? Or did the packet… never exist at all?

def encryptedSize (fileSize, chunkSize):
return (fileSize / chunkSize) * (chunkSize + 16) + (fileSize % chunkSize) + 16

Here we see a bit of code to get the size of the file that the device is waiting for. The device will wait until such time as it gets the whole file. Now, this file is being chopped into chunks and encrypted. Each chunk will have a little extra in it (16 bytes) because it’s encrypted. So how much bigger will the file be? Well, as you can see, we divide the file by the size of the chunks, and multiply that by the chunk size plus 16. Then we add in the remainder, what’s ever left over. Then we add 16 more for the size of this last chunk.

This works great, almost all of the time. You might already see the issue, though. You can think of this as putting pizza slices into boxes. Each box holds 8 slices. How many boxes do you need? Divide the number of slices by 8, of course. And then you’ll have one more box for whatever is left over. So add one. That’s more or less what the code above does.

What if you have 16 slices? Well, 16/8 is 2, so you fill two boxes with pizza. Then you take what’s left and put it in another box, so that’s 3 total. Wait, there’s nothing left? Why would you send out an empty box? Listen, you! That’s a very good question.

So yes, this will fail when the remainder is zero. Now, our chunk size is much more than ’8′ so the failures are going to be very, very rare. But they’ll happen. Oh, they’ll happen. Fortunately, the fix is simple, once you know the problem: Don’t send out empty pizza boxes.


def encryptedSize (fileSize, chunkSize):
if (fileSize % chunkSize) > 0:
return (fileSize / chunkSize) * (chunkSize + 16) + (fileSize % chunkSize) + 16
else:
return (fileSize / chunkSize) * (chunkSize + 16)

The Case of the Malformed Pickle

Another class of problems are known as ‘race conditions’. The name comes from analog wiring, and the idea of two signals ‘racing’ to the end of the circuit. If one gets to a certain component first, you’ll have a different result than if the other one does. In programming, we get something similar when two processes are accessing the same data store.

In Python, we ‘pickle’ objects, which is just an overly-clever way of saying that we change them into a format suitable for storing. In one of our systems, we would occasionally get an EOF (end of file) error while trying to access a pickle. This, however, turned out to be a bit of a red herring, since no file was being accessed. Why was there a red herring in our pickle? Well, turns out the REAL error was that the data was encrypted, and something was wrong with the decryption. Thus, we were trying to de-pickle a string of still-encrypted data. This was discovered by checking the logs and seeing that the device was sending a key that didn’t match what the server had stored. But the device gets the key from the server… what’s going on here?

Well, let’s go back to 2003; when on a sunny Summer day the lights went out… all over the world. Well, okay, it was just the northeast U.S. and Canada, but that’s still a pretty big deal. There were a variety of reasons for the blackout that we won’t go into here, but the most interesting one occurred right here in the great state of Ohio, where a bug called a ‘race’ condition caused an alarm not to go off. Now, generally, a single alarm in a power control room in Ohio shouldn’t take down the whole eastern seaboard, but that’s another story.

My understanding of what happened is basically this: let’s say that when a power line sags and hits a tree and is knocked off line (happens a lot in the summer) you want to know about it. And let’s say when three of these occur in a certain time frame, you need to take some recourse (hitting the big button that says ‘don’t let the entire east coast go dark’). So the code would look something like this: (This isn’t the actual code used… I hope.)


10 PRINT "TreeHitLine Oh Noes"
20 GET NumberOfTreeAlarms FROM box
30 SET NumberOfTreeAlarms to (NumberofTreeAlarms + 1)
40 PUT NumberOfTreeAlarms IN box
50 IF NumberofTreeAlarms >= 3 THEN WhoopWhoopWhoopBigAlarm

Looks good, huh? So what’s the problem? Well, let’s say this program is being run by multiple ‘processes’ that use the same box and run at the same time.  So imagine signals coming in from two tree alarms at the same time, one in Parma and one in Solon. They’ll both run line 20 and get the value in the tree alarm box. Let’s say it’s one. Parma reads the box and gets one. Then Solon reads the value… after Parma read it, but before Parma stored the new value in the box. These things don’t happen instantly, after all. They’ll both read one, and add another one and get two. Then each one will store that value in the box. Line 50 will not trip the big alarm, since the value isn’t three; it’s two. Alarm doesn’t go off, 55 million people have to eat all the ice cream in their freezers. That’s a race condition.

So where does that leave us with our pickle? Well, have a look at this code:


c = Crypto ()
if 'url_encryption_key' in session:
key = session['url_encryption_key']
else:
key = c.new_key ()
session['url_encryption_key'] = key
session.save ()

So, we need a key. If we’ve got one, we use it. If not, make a new one. Sounds good. But what if two requests come in at the same time? Well, we might have two see that there’s no key, and make one. Then, both will save to the session. But of course, the second one over-writes the first, so that first key is now invalid. The server will read the key from the session, do a compare, and throw out any requests with a non-matching key… so any requests that use the first one will fail, utterly.

Now, what if the device attempts to get all of the chapters for a book at one time, using a separate request for each chapter? That is pretty much what it does, and that just might cause some problems. We’ve got a race condition.

How do you fix a race condition? It isn’t easy. Really making things ‘thread safe’ can involve crazy things like semaphores and atomic transactions. Fortunately, though, in this case, we can greatly decrease the chance that we’ll see trouble by simply having the program check the current key before it sends out the data to the device:

key = session['url_encryption_key']

Since the new key is only created when the first request comes through, we don’t have to worry about race conditions happening after that first one, which is one of the things that makes fixing them so tricky… you just introduce a new race condition in fixing the first one. But here, all we need is for there to be one key for all requests. Whatever that key ended up being isn’t important; so we just make sure to pull it when we need it instead of assuming it’s the one our process chose. Of course, there can still be problem if the key is read the second time before the save occurs in the other thread. That’s unlikely here, but it could happen.

Also not that in the above code, the session isn’t reloaded right before the first check. That’s a problem that makes this worse (in a way – we’re not shutting down 1/4th of the country) than the Blackout bug… the ‘session’ variable here isn’t the “actual” session, but a copy we have. We’re not looking in the box, we’re looking at a picture of the box, that could be from awhile ago. Each process might be using outdated information and think they need a new session key when it really doesn’t. So let’s add that in as well:


session = get_session ()
c = Crypto ()
if 'url_encryption_key' in session: ...

With both of these fixes in place, we haven’t seen a bad pickle in a few days. Is it still possible in theory? Yes. But we certainly won’t see a few a day, as was the case previously. A solution that truly prevents race conditions will be a bit more work. But this should be enough to save the ice cream.

]]>
/development/edges-corners-and-malformed-pickles/feed/ 0
Extending NetSuite with the SuiteScript API /development/extending-netsuite-with-the-suitescript-api/ /development/extending-netsuite-with-the-suitescript-api/#comments Thu, 09 May 2013 17:30:06 +0000 /?p=1319 SuiteScript is a JavaScript-based API that gives developers the ability to extend NetSuite beyond the capabilities provided through SuiteBuilder point-and-click customization.

The majority of NetSuite forms, records, customization objects and their event/trigger points are programmatically accessible through SuiteScript. What you decide to do with SuiteScript depends on which part of NetSuite you are trying to extend, search, or process.

What can I do with the SuiteScript API?

When you think about using SuiteScript in NetSuite, you must ask yourself:

1.    What do I want to do?

2.    Which APIs support what I want to do?

3.    How do I run a script in NetSuite?

What do I want to do?

The following are just some of the uses for SuiteScript. Next to each use case is a link to the NetSuite script type you might use to programmatically accomplish the tasks involved. There are hyperlinks in blue in the items below. These link to the NetSuite Help Center so that you can get additional information for specific areas. To access these links you must have a NetSuite Account.

Using SuiteScript you can:

•   Perform custom business processing when NetSuite records are updated, created, deleted (using User Event Scripts).

•   Perform custom validations and calculations in the browser client (using Client Scripts).

•   Create custom user interfaces (using script types such as Suitelets or User Event Scripts and UI Objects).

•   Run batch processes (using Scheduled Scripts).

•   Execute NetSuite searches (using script types such as User Event Scripts or Scheduled Scripts).

•   Perform various utility processing such as sending email and faxes, creating and uploading files, or working with XML documents (using script types such as User Event Scripts or Suitelets).

•   Create custom dashboard portlets (using Portlet Scripts).

•   Perform processing in target accounts for bundled solutions as part of bundle installation or update (using Bundle Installation Scripts).

 In the documentation, the SuiteScript API is organized by the types of tasks most developers want to perform. See SuiteScript API Overview to get started with the SuiteScript API.

See SuiteScript Functions to see how all APIs are organized. The documentation for each API lists whether the API can be used in client, user event, scheduled, Suitelet, or portlets scripts.

How do I run a script in NetSuite?

 The overall process for getting a script to run in NetSuite is fairly basic. This process includes:

1.  Creating a JavaScript file for your script.

2.  Uploading the file into NetSuite.

3.  Creating a NetSuite “Script” record for your script.

4.  Defining script runtime options on a NetSuite Script Deployment page.

Step 1: Create Your Script

All SuiteScript files must end with a JavaScript (.js) file extension. Although you can use any text editor (including Notepad) to write your SuiteScript .js files, NetSuite recommends you use the SuiteCloud IDE. Using the SuiteCloud IDE will be covered in a future blog.

Depending on what you are trying to do in NetSuite, the code in your .js file can be as simple as a client script that never even touches a NetSuite server. It runs purely client-side in the browser and alerts users after they have loaded a specific NetSuite record, for example:

                   function pageInitAlertUser()
                  {
                  alert (‘You have loaded a record’);
                  }

Alternatively, your script can be as complex as executing a NetSuite search, getting the results, and then transforming the results into a PDF document.

The APIs you use in your code and the logic you write will depend on what you’re trying to accomplish in NetSuite.

Step 2: Add Script to NetSuite File Cabinet

If you are writing your script files in SuiteCloud IDE, loading a file into the NetSuite file cabinet is as easy as right-clicking on your file in SuiteCloud IDE and selecting NetSuite > Upload Selected File(s).

If you have written your .js files in anything other than SuiteCloud IDE, you will need to manually upload your files into NetSuite. See Uploading SuiteScript into the File Cabinet Without the SuiteCloud IDE for details.

Note:  

The SuiteScripts folder in the file cabinet is provided for convenience, however, you can store your script files in any location.

Once your script has been added to the NetSuite file cabinet, see either:

 Step 3: Attach Script to Form (if you want to run a form-level client script in NetSuite)

Step 4: Create Script Record (if you want to run any other script type. For example, if you want to run a user event, scheduled, portlet, Suitelet, action, or record-level client script, proceed to Step 4.)

Step 3: Attach Script to Form

Form-level client scripts are simply “attached” to the forms they run against. Be aware that in NetSuite, there are two different types of client SuiteScript. The information in this section pertains ONLY to form-level client scripts.

To attach a form-level client script to a custom form:

1.    Ensure your client script has been uploaded into NetSuite.

2.   Go to the desired custom form in NetSuite. Form-level client scripts can only be attached to custom entry forms, custom transaction forms, and custom online forms. Click Setup > Customization > [Form].

3.   Next, click Customize next to the desired custom form, or click Customize next to an existing standard form in order to create a new custom form based that is based on the standard version.

4.   On the Custom Code tab on the form, select your script file and, if necessary, the library file to associate with the current form (see figure below).

5.    The library script file should contain any commonly used functions. The SuiteScript file should contain functions specific to the current form.

6.    Based on the APIs used in your SuiteScript or library files, define which functions should be called on which client events. If you are unsure of which actions trigger each client event, see Client Event Types.

The following figure shows a custom form called Wolfe Electronics Sales Order. This form is set as the “preferred” form for sales orders. What this means is that all customizations made to this form, and any client script file attached to the form will run whenever a NetSuite user navigates to and loads sales order record. This figure indicates that when a sales order loads, three different functions will execute.

  •  The savRecUpdatePrice function will execute when the record is saved.
  • The valFieldItemPrice function will execute when a particular field on the sales order is changed.
  • The recalcTotalAndTax function will execute when a line item has been added to a sublist.

Important: 

Be sure to enter function names exactly as they appear in your script. However, do not include parenthesis or parameter values when you enter the function name on the custom form.

CustomerForm

After you have attached your form-level client script to a form, your script will execute whenever the triggering action occurs. For a list of possible client event triggers, see Client Event Types.

If you have created a form-level client script, you do not need to go to Step 4: Create Script Record . You are done!!!!!

Step 4: Create Script Record

After writing your SuiteScript .js file and uploading the file to the file cabinet, you must then create a Script record for the file (see figure below).

On the Script record you will:

•   Add your SuiteScript .js file.

• Define the script owner.

•  If applicable, add one or more library files.

• Define the function(s) from your SuiteScript file you want executed.

• If applicable, create script parameters (custom fields) that are unique to the Script record.

• Specify who should be contacted if an error is thrown in your script.

Although you do not need to set every field on the Script record, at a minimum you must set the following (see figure below):

1.   Provide a name for the Script record.

2.   Specify the script owner.

                          3.   Load your SuiteScript .js file.

4.   Specify the main executing function within the file.

NewScript

Future Blog

In my next blog, I will take an in depth look at the SuiteCloude IDE!

]]>
/development/extending-netsuite-with-the-suitescript-api/feed/ 0
A Crack in the Armor /development/a-crack-in-the-armor/ /development/a-crack-in-the-armor/#comments Thu, 02 May 2013 12:00:43 +0000 /?p=1306 It’s been almost two months now since we went live with our first Magento Commerce site.

Over all, I think it has been a real success story. There have been a few bugs, and some customer inquiries, but not as many as I expect with the first implementation of a new technology.
We have been more flexible in addressing problems and implementing solutions since the rollout, and that looks to continue.

However, I seem to have found the first major flaw with the Magento framework.

We have 27 custom modules, where we rewrite core code, implement new functionality, and configure the system. Many of the changes are relatively trivial, but a few are core to the customer experience. It is great how easy Magento is to customize, but it is these custom modules that now present us with our first problem: Magento doesn’t provide a way to say which set of modules are being used for a particular website or store. You can configure the output to be disabled for a module at the website or store level, but the rewrites, observers, and other back end code are all still active. I have been in contact with Magento Enterprise support, consulting groups we have relationships with, and of course the standard Google, and nothing has led me to an out of the box solution for this.

module-shock

So as we start down the path of ramping up additional sites and stores, we will have to pay particular architectural attention to how we implement custom code that we might not want to have executed on all of our sites.

I am still very pleased with our decision to implement Magento, but no product is perfect…

]]>
/development/a-crack-in-the-armor/feed/ 0
Question Everything /ux/question-everything/ /ux/question-everything/#comments Mon, 29 Apr 2013 14:30:36 +0000 /?p=1236 If there’s one thing I’ve learned from all the conferences, talks, and books that I’ve read about designing user experiences, it’s this:

Design for your audience, not for yourself.

Sound simple? Nope. Designing for your audience means questioning everything, and it turns out that’s really hard.

There are simply some things that you take for granted, things that you mistake for universal truths that really only apply to you. Maybe it’s the way your parents or a teacher showed you how to do something, or maybe it’s something that you don’t witness many other people doing. Everyone thinks they do things in the most logical, sensible, efficient, ‘right’ way (especially programmers). This is why it’s so tempting to design solutions for yourself, and why UX professionals are so valuable (I do not consider myself a UX professional).

Take something seemingly simple like reading a book. I was recently throwing around some ideas for a bookmarking experience. Even while consciously trying to stay objective, I soon discovered that I was assuming something very fundamental to the experience based on how I personally do things. All 3 people in the room at the time had different reading/bookmarking behaviors.

Will

When I feel like putting down a book, I push through and keep reading until I get to the next page. I read up until the first full paragraph on that page, then stop. I either dog-ear the page, or face the ‘front’ of my bookmark towards the last page I read. When I pick it back up, I look for the first indent and start reading from there.

Rob

Rob stops reading as soon as he feels like stopping, or when he gets interrupted. He bookmarks the page, and when picking it back up, scans the page(s) for the last thing he remembers reading, and continues from there.

Shawn

Shawn also stops reading right where he feels like stopping, but uses a page-clip bookmark that he can point to the last line he read, so he can pick the book right back up without scanning the page to find where he left off.

3 people in the room, 3 different reading habits. The only way to uncover this sort of thing is to ask questions and do research. Show prototypes and ideas to other people early and often. Iterate on your ideas and don’t wait to uncover them or talk about them until you feel they’re ‘finished.’ This experience was a great reminder to stay vigilant in never assuming.

]]>
/ux/question-everything/feed/ 0