Dec 21

Optimizely beginners tips & tricks

We recently started experimenting with A/B-testing at our company, and are currently checking out Optimizely. While getting started some problems have popped up that took me a little bit of time to iron out. I’m writing this in the hope of saving someone else going through the same hassle.

For instructions on performing the actual installation you’re better of following Optimizely’s own getting started guide.

Some experiments should not run simultaneously

Optimizely lets you run multiple experiments at the time. To get results that you can trust from your experiments you probably want to avoid them interfering with each other. If your experiments changes the same part of the page, or the goals of one experiment could be affected by the changes made by another, you better make sure a single user is not exposed to both experiments at the same time.

To accomplish this, you can add the following javascript snippet, borrowed from Optimizely’s learning center, in each experiment’s targeting setup (Options -> Targeting):

// Setup array of mutually exclusive experiment ids
expArray=[<<experiment_id1>>,<<experiment_id2>>,<<experiment_id3>>];
// Set the id for the current experiment being evaluated
curExperiment = <<experiment_id_current>>;
// Optional. Needed if excluding multiple groupings
groupName = "groupA";

/*--  Do not modify below this line --*/
/*  Internal comments below:
    Iterate over current bucket mappings and
    set the global variable to the experiment
    the user is already included in.
*/
groupName = groupName || "groupA";
// Safety
groupName = "__" + groupName;

for(var key = 0; key < expArray.length; key++) {
     if (document.cookie.indexOf(expArray[key]) > -1) {
          optimizely[groupName] = expArray[key];
          break; // we found what we're looking for so end loop
     }
}

// If the global variable hasn't been set, set it now at random.
optimizely[groupName] = optimizely[groupName] || expArray[Math.floor(Math.random()*expArray.length)];
// Check if the current experiment matches the global experiment.
optimizely[groupName] == curExperiment;

You need to add the ID of all experiments that are to be run exclusively to the expArray variable, and the ID of the current experiment to the curExperiment variable. These IDs can be found by navigating to Options -> Diagnostic Report in each experiment.

Make sure every single element gets changed

Some of the experiments we are running make changes to multiple elements of the same type, for example in a

eksperimentsearch result. I noticed that on some page loads all elements did not get changed properly, and I’d end up with something looking rather broken, like what you see on the right. I’m confident this would affect the results of the experiment.

The Optimizely snippet seems to start (and finish) doing its thing before the page is fully loaded. Optimizely support did not figure it our either, but they did give me a workaround.

The workaround is to make sure the variation code runs in a loop until all elements are changed. Add the following javascript snipped in the experiment code editor:

/* Don't touch this code */
function waitForDelayedContent(selector, experiment, timeout, keepAlive) {
var intervalTime = 50;
var timeout = timeout || 3000;
var keepAlive = keepAlive || false;
var maxAttempts = timeout / intervalTime;
var attempts = 0;
var elementsCount = $(selector).length;

experiment(); // Run once first, no matter what.

var interval = setInterval(function() {
if ($(selector).length > elementsCount) {
if (!keepAlive) {
clearInterval(interval);
}
experiment();
elementsCount = $(selector).length;
} else if (attempts > maxAttempts) {
clearInterval(interval);
}
attempts ++;
}, intervalTime);
}
/* --------------------------------------------- */

Then pass your variation code to this function:

waitForDelayedContent(<CSS SELECTOR FOR ELEMENT TO CHANGE>, function(){

// Your variation code here

}, 10000);

For larger changes, implement both versions in your own code

When making large changes to the page, I found it a hassle spending lots of time making changes to both HTML and CSS in Optimizely’s own editor(s) and then, after the experiment finishes, having to extract all the changes and re-implement them in my own code.

What I’ve done, whenever I’m making changes that are more substantial than one or two simple changes to text, colors, size etc, is to implement both versions in my own code and push this into production. I hide the experiment code by default, and flip between the two using the variation code in Optimizely.

Your applications source code could then be something like this:

<div class="original">
  // Current stuff here
</div>

<div class="experiment" style="display: none;">
  // Experiment here
</div>

When this code is put in production, your variation code in Optimizely can be as simple as:

$(".original").remove();
$(".experiment").show();

Nov 03

Good driver in Kandy, Sri Lanka

If you need a good driver in Sri Lanka, and perhaps particularly around the Kandy area, let me tell you about a very nice guy we met on our current trip.

When travelling, sites like tripadvisor.com is of great value when looking for places to stay. Having had a really good driver for a couple of days on our current trip to Sri Lanka, I was a bit sorry not to be able to recommend him like we do the places we stay, so I thought I’d write a small post here, in the hopes that it will help both fellow travelers as well as the driver.

IMG_20131103_143713-small

We met the driver by chance at arrival by the Kandy railway station, and he drove us to the place we were staying. We got his contact details, and since he seemed like such a pleasant guy, we later hired him for driving us on a two-day trip to Madulkelle Tea and Eco Lodge for what was a very reasonable price.

He’s a no fuzz kind of guy, not in any way pushy, but always suggested various sights and experiences on the way and taking detours to let us experience them. He even stopped at a small tea factory near Madulkelle, where he’d never even been, and got the manager to give us a personal tour. Sweet!

His full name, according to his business card, is Bandara Dessanayake, but he uses Sam towards tourists. Slightly easier to remember. His car is a Toyoya Camry, if memory serves me correct, probably from sometime around 2000. Nothing fancy, but does the job more than well enough. His phone number is +94 77 5796131.

Thanks for being a great guide and driver during our visit to Kandy, Sam!

Oct 15

Urettmessig tilleggsavgift på bompassering? En telefon kan lønne seg.

Vi fikk nylig en faktura i posten fra Østfold Bompengeselskap AS (en del av Bro- og tunnelselskapet AS), pålydende 320 kroner. 20 kroner for bompassering, og 300 i “tilleggsavgift” (les: bot) for ikke å ha betalt. Det var bare et par problemer:

  • Vi betaler for oss når vi passerer bomstasjoner med myntinnkast, og forsikrer oss om at lyset er grønt før vi kjører.
  • Vi bor og jobber i Oslo, og følte oss ganske sikre på at vi ikke passerte en bomstasjon i Moss klokken 06:12 denne morgenen.
  • Den aktuelle morgenen var over 3 måneder før fakturaen ble sendt ut.

Tilfeldigvis var omstendighetene slik at vi husker å ha passert (og betalt) denne bomringen på ettermiddag/kveld to dager etter den angivelige snikingen. Helt tilfeldig kunne vel ikke tilleggsavgiften være, når det dreier seg om en bomring vi så godt som aldri passerer?

Selv om utallige frustrerte innlegg på nettet tilsier at en klage på tilleggsavgift for bompassering er fånyttes, tok vi en telefon for å i det minste se hva de hadde av bevis. Og bevis fantes ikke, i alle fall ikke mot oss. I følge selskapet var bildet de satt på av et helt annet skilt, og de frafalt kravet på stedet. Kjekt, men ikke spesielt tillitsvekkende.

Noe sier meg at en god andel av mottakerne av denne typen tilleggsavgifter ser det som så usannsynlig at de kommer noen vei med klagen sin at de betaler uavhengig av skyld. Jeg skriver dette i håp om at noen i det minste tar den ene telefonen for å sjekke om bildet de sitter på er av ens eget skilt.

Jun 13

A couple of new Android apps

Over the last couple of weeks, I’ve created a couple of small apps for fun, to scratch my own itch, and to experiment a little with Android Market. Check them out:

Headphone Action

Small utility for starting an app of your choice whenever your headphones are plugged into your device. My first paid app, to see how that all works. Market has a cap on how low you can set the price, so it’s priced about the minimum price for all countries (rounded off a bit where possible). Go to Headphone Action on Android Market

Næringsinnhold

Norwegian-only app, with nutritional info for over 1000 common Norwegian food items and drinks. It’s a free app, and the first I’ve made with ads (by AdMob), to check out how that works. Let’s just say I’m not browsing the yellow pages for Ferrari dealerships just yet. I’ve tried to keep the ads non-intrusive in coloring and in the categories of ads that are displayed, which results in ads only showing about 1/3 of the time. This bit is up for a bit of experimentation. Go to Næringsinnhold on Android Market.

Now.. what to make next?

Apr 19

Android: Handling longpress/longclick on map (revisited)

I’ve prevously written about my attempts to capture longpress/longclick on a map (see this post). As I wrote in that post:

“While working on my MapActivity based Android app, I wanted to be able to show a context menu when the user longpressed any point on the map (not necessarily a marker in an overlay) and perform some action related to that point. I wrongly assumed that there would be some simple method to override for this, like onLongPress(). ”

The solution I outlined in the previous post never did feel quite right. It was a bit of a mess and it could be difficult to follow what was really happening. I recently improved this design in an app of mine, and here’s the lowdown:

The main steps are as follows:

  • Subclass MapView and define the OnLongpressListener.
  • In your layout file, set up the new MapView subclass.
  • Define an instance of your new OnLongpressListener in your MapActivity.

The first point is the most important, this is the core of the solution: Subclassing MapView and adding our custom OnLongpressListener. I hope the code comments make it clear enough what is going on:

public class MyCustomMapView extends MapView {
        
        // Define the interface we will interact with from our Map
	public interface OnLongpressListener {
	    public void onLongpress(MapView view, GeoPoint longpressLocation);
	}
	
	/**
	 * Time in ms before the OnLongpressListener is triggered.
	 */
	static final int LONGPRESS_THRESHOLD = 500;
	
	/**
	 * Keep a record of the center of the map, to know if the map
	 * has been panned. 
	 */
	private GeoPoint lastMapCenter;
	
	private Timer longpressTimer = new Timer();
	private MyCustomMapView.OnLongpressListener longpressListener;

	
	public MyCustomMapView(Context context, String apiKey) {
	    super(context, apiKey);
	}

	public MyCustomMapView(Context context, AttributeSet attrs) {
	    super(context, attrs);
	}

	public MyCustomMapView(Context context, AttributeSet attrs, int defStyle) {
	    super(context, attrs, defStyle);
	}

	public void setOnLongpressListener(MyCustomMapView.OnLongpressListener listener) {
		longpressListener = listener;
	}

	/**
	 * This method is called every time user touches the map,
	 * drags a finger on the map, or removes finger from the map.
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		handleLongpress(event);
		
		return super.onTouchEvent(event);
	}

	/**
	 * This method takes MotionEvents and decides whether or not
	 * a longpress has been detected. This is the meat of the
	 * OnLongpressListener.
	 * 
	 * The Timer class executes a TimerTask after a given time,
	 * and we start the timer when a finger touches the screen.
	 * 
	 * We then listen for map movements or the finger being
	 * removed from the screen. If any of these events occur
	 * before the TimerTask is executed, it gets cancelled. Else
	 * the listener is fired.
	 *  
	 * @param event
	 */
	private void handleLongpress(final MotionEvent event) {
		
		if (event.getAction() == MotionEvent.ACTION_DOWN) {
			// Finger has touched screen.
			longpressTimer = new Timer();
			longpressTimer.schedule(new TimerTask() {
				@Override
				public void run() {
					GeoPoint longpressLocation = getProjection().fromPixels((int)event.getX(), 
							(int)event.getY());
					
					/*
					 * Fire the listener. We pass the map location
					 * of the longpress as well, in case it is needed
					 * by the caller.
					 */
					longpressListener.onLongpress(MyCustomMapView.this, longpressLocation);
				}
				
			}, LONGPRESS_THRESHOLD);
			
			lastMapCenter = getMapCenter();
		}
		
		if (event.getAction() == MotionEvent.ACTION_MOVE) {
				
			if (!getMapCenter().equals(lastMapCenter)) {
				// User is panning the map, this is no longpress
				longpressTimer.cancel();
			}
			
			lastMapCenter = getMapCenter();
		}
		
		if (event.getAction() == MotionEvent.ACTION_UP) {
			// User has removed finger from map.
			longpressTimer.cancel();
		}

	        if (event.getPointerCount() > 1) {
                        // This is a multitouch event, probably zooming.
	        	longpressTimer.cancel();
	        }
	}
}

The layout file will look somewhat like the following:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<com.example.MyCustomMapView android:id="@+id/mapview"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
		android:apiKey="<YOUR API KEY HERE>"
		android:clickable="true"/>
</RelativeLayout>

Make note of the android:clickable attribute. As you might know, this must be set to be able to pan, zoom or in other ways interact with your map.

The final piece is adding your onLongpressListener in your MapActivity. For the sake of the example, let’s say the previous layout file is named res/layout/map.xml. The necessary code for your MapActivity will look something like this (I only include the parts relevant for this example:

public class Map extends MapActivity {
    private MyCustomMapView mapView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.map);
        mapView = (MyCustomMapView)findViewById(R.id.mapview);

        mapView.setOnLongpressListener(new MyCustomMapView.OnLongpressListener() {
	    public void onLongpress(final MapView view, final GeoPoint longpressLocation) {
	        runOnUiThread(new Runnable() {
		    public void run() {
		        // Insert your longpress action here
		    }
		});
	    }
	});
}

To actually have your longpress open up a context menu, you need to perform some additional setup of the context menu itself. I’ve avoided including this, to make the example clearer. Any tutorial on setting up context menus should be able to guide you in the right direction.

If something is unclear in the examples, or you have trouble getting stuff working, just add a comment and I’ll try to help you out.

Older posts «