Analytics in web games and how to cook them

What feedback options do you have when writing in Inform, or Undum, or Twine, or any other popular IF engine?

You have betatesting, some player reviews on IFDB and some critique articles on IF Planet blogs. If you’re lucky, there can be Twitch streams and Youtube too.

Web games can have more than that.

Just the basics

The easy way is to just install some plain analytics script and be done with it. The analytics scripts are very powerful nowadays.

You will learn about the devices your game starts on. Tablets? Mobiles? Desktop computers with 1024x768 EGA monitors? Who knows!

Maybe your script will tell you that your Sci-Fi Pulp Erotic Lesbian Adventure attracts mostly Canadian bearded males in 14-40 age range… okay, scratch that.

You can view what sites give you more players, what sites give you most devoted players (look for average playing time).

But this is dancing around the game. What happens inside?

We have to dissect the game to dissect the audience.


Click to enlarge.

It’s a screenshot from my Piwik installation, stats for one of my WIPs. The only person playing the game is the author, that’s why the stats are so low. But they are live, the game collects and analyzes them.

Piwik thinks these are pages, but in reality there are only “rooms”, or smart passages. Every room has an internal title, and Piwik treats it as a separate page.

What do I get out of that?

  • “Pageviews”: how many times people entered this room
  • “Unique pageviews”: how many people entered this room
  • “Exit rate” shows how many people gave up and closed the game standing in this room
  • “Bounce rate” shows how many people did it right after entering (15 seconds or fewer)
  • “Avg. time on page”: how much time people spent in this room
  • “Avg. generation time” is not useful at all because the game is 100% client-side, so every room loads almost instantly.

Now, how to do it with Salet. (These recipes should be good for Raconteur too but you’ll have to tweak the syntax a bit.)

The recipe: passage analytics

croom = (title, salet, options) ->
  options.before = (salet) ->
    _paq.push(['trackPageView', title]) # <--- THIS IS IT
  return room(title, salet, options)

croom "begin", salet,
  tags: ["start"],
  optionText: "begin_title".l()
  choices: "#start2"
  dsc: (salet) -> "begin".l(salet)

Obviously, you have to connect Piwik too (the usual way, just add <script> tag somewhere)

I’ll try to be short with the technical explanation: in this code block I make a new room type, croom, and construct it with custom before callback that calls Piwik API. Then I use it. The l() functions are for localization (my games are bilingual).

UPD: when using this code, you will not be able to set before callbacks anymore. It’s not a big problem in Salet, you still have enter functions. If you are on Raconteur, though, you’ll have to be careful with that, maybe define a new realBefore property or something.

Okay, this is cool, but I’m not finished yet. For my shooting game, I wanted to have Telltale-like graphs, like “5% of people shot that robot”.

And this is why I recommend Piwik over Google Analytics, Yandex.Metrica and other solutions. It’s super easy to do this:


Click to enlarge.

It’s not a perfect chart, because you have to know that 0 stands for “No”, 1 stands for “Yes” and the “Value not defined” stands for “The player blocked the analytics gathering somehow or did not play long enough to view that event”. But it’s easy enough to make.

The recipe for custom charts

First, you have to install the Custom Dimensions plugin on your Piwik instance.

Then you create a Custom Dimension.

Scope "Visit" is for something that you want to track once per playthrough, like “found the key” or “finished the game” are things that won’t change after the player got them. They are NOT TRUE on the game start but become TRUE later on, and you want to track that change.

The "Action" scope is for something that you want to track in all possible states for all playthroughs. You can use that to track room views too, but I didn’t test that.

Then you do something like this:

before: (character, salet) ->
  salet.character.shot_pacifist = 1
  _paq.push(['setCustomDimension', 2, salet.character.shot_pacifist])

Hmm. I guess I could use “true” and “false” instead of “1” and “0” but– oh well.

Now what?

You can these tricks in betatests or releases.

You can look at your branches and see what choice is more popular.

You can look at your puzzles and see how many people get stuck at what point.

So I trust you to check this pulse of your game and get better with it.