Labs/Test Pilot/ExperimentAPI: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
Line 2: Line 2:


Test Pilot experiments and surveys are implemented as Javascript Securable Modules.  They will be loaded using Cuddlefish.  They are expected to export objects with certain specific names.
Test Pilot experiments and surveys are implemented as Javascript Securable Modules.  They will be loaded using Cuddlefish.  They are expected to export objects with certain specific names.
Test Pilot experiments run with full chrome privileges and can do everything that extensions do.  With great power comes great responsibility!


Test pilot experiments and surveys are hosted on https://testpilot.mozillalabs.com.  This URL is hard-coded into the Test Pilot extension.  Note that the experiments and surveys are served exclusively over SSL to reduce the possibility that a man-in-the-middle could substitute malicious code.
Test pilot experiments and surveys are hosted on https://testpilot.mozillalabs.com.  This URL is hard-coded into the Test Pilot extension.  Note that the experiments and surveys are served exclusively over SSL to reduce the possibility that a man-in-the-middle could substitute malicious code.


== The Index files ==
== The Index files ==

Revision as of 01:26, 21 May 2010

General

Test Pilot experiments and surveys are implemented as Javascript Securable Modules. They will be loaded using Cuddlefish. They are expected to export objects with certain specific names.

Test Pilot experiments run with full chrome privileges and can do everything that extensions do. With great power comes great responsibility!

Test pilot experiments and surveys are hosted on https://testpilot.mozillalabs.com. This URL is hard-coded into the Test Pilot extension. Note that the experiments and surveys are served exclusively over SSL to reduce the possibility that a man-in-the-middle could substitute malicious code.


The Index files

The server holds an index file which lists experiments to run. The clients check this file (on startup, then once per day thereafter) and then download the experiments they find listed there. To release a new experiment or survey, we add it to the index file. To stop giving out an experiment or survey, we take it out of the index file. (Clients who already have it will keep running it).

The canonical index file is at https://testpilot.mozillalabs.com/testcases/index.json. You can go have a look at it right now. It's human readable and fairly self-explanatory.

The top level of the JSON has a property called experiments, which is an array of objects with names and filenames:


{"experiments": [{"name": "Tab Open/Close Study",
                 "filename": "tab-open-close/tabs_experiment.js"},
                {"name": "Survey For New Test Pilots",
                 "filename": "new_pilot_survey.js"}]}


In order to develop and test experiments that we're not ready to deploy, we have an alternate index file for developers only. This lives at https://testpilot.mozillalabs.com/testcases/index-dev.json. To make your copy of the Test Pilot client use this file:

# go to about:config
# find the pref called extensions.testpilot.indexFileName
# change its value to index-dev.json

Once you make that switch, you will start getting all the studies that are still in development, as well as some dummy studies which exist for testing purposes only. If you want to develop your own study, you should start using index-dev.json.

Version control for experiment/survey code

Experiment and survey code lives in the Testpilot website Hg repository: http://hg.mozilla.org/labs/testpilotweb .

Specifically, index.json is in the testcases directory within this Hg repository. Since our website is just a checkout of this repo, that puts the file at a URL of https://testpilot.mozillalabs.com/testcases/index.json . The paths and URLs of all survey/extension code are relative to index.json.

Working with Cuddlefish

See https://wiki.mozilla.org/Labs/Jetpack/JEP/28 for detailed information about using Cuddlefish.

Since your experiment code is a Cuddlefish securable module, it has access to the following globals...

To import another securable module:

var module = require( moduleName );

To export functions, constants, variables, or objects:

exports.foo = foo;

To provide destructor/cleanup code to be run when your module is unloaded:

require("unload").when(function myDestructor() {
    // do stuff here
  });

To log informational or error messages:

console.debug("Timestamped an event.");
console.info("Something happened.");
console.warn("Uh-oh: something happened.");
console.error("Something bad happened!!");

You have access to XPCOM through the following defined constants:

  • Cc is mapped to Components.classes.
  • Ci is mapped to Components.interfaces.
  • Cu is mapped to Components.utils.
  • Cr is mapped to Components.results.

Surveys

Surveys are very simple to write. You merely need one js module file that exports an object called surveyInfo. The surveyInfo object must have the following properties:

  • surveyInfo.surveyId: a unique string that identifies this survey. Can be anything but must be distinct from the ids of any other surveys. Will be used as part of a Mozilla preference so it must not contain spaces.
  • surveyInfo.surveyName: a string containing the human-readable name of the survey, which will be displayed in the extension's menus, etc.
  • surveyInfo.summary: A couple sentences to be displayed in the Test Pilot interface explaining what the survey is about.
  • surveyInfo.thumbnail: A URL to an image file that will be displayed (at 90x90 pixels) next to the summary.
  • surveyInfo.versionNumber: Required. The version number of the survey. Increment this number when you change the survey questions, so that when you're looking at survey results you can tell which version they came from.
  • surveyInfo.minTPVersion: A version string (e.g. "1.0a1") corresponding to a version of the Test Pilot extension. Versions of the extension older than this will not attempt to download the survey. If not specified, all versions of the extension will download.
  • surveyInfo.uploadWithExperiment: Optional. If this survey goes with another study, then set this to the ID of that study to ensure that the survey results and study results get uploaded together and can be correlated.
  • surveyInfo.surveyQuestions: Required. An array of question objects. See the example file below to see how to write question objects.

The presence or absence of the surveyInfo object is used by the extension to determine whether a JS module is a survey or an experiment. Any JS module that exports an object named surveyInfo will be treated as a survey. If you are writing an experiment, make sure not to export an object named surveyInfo!

Example

There is an example survey in the hg checkout, at

testcases/example/example_survey.js

You can also see it on the web right now at https://testpilot.mozillalabs.com/testcases/example/example_survey.js. This survey is pretty silly but it demonstrates the json format for defining survey questions and possible answers.

Experiments

Example Experiment

There is an example experiment file in the hg checkout at testcases/example/example_study.js. You can also see the code for it on the web right now by going to https://testpilot.mozillalabs.com/testcases/example/example_study.js. It is heavily commented and intended to be as self-explanatory as possible. The best way to get started writing an experiment is probably to copy that file and start changing stuff.

Experiment API definition

Experiments are much more involved than surveys, and must export four objects, named exactly as follows:

  1. experimentInfo
  2. dataStoreInfo
  3. webContent
  4. handlers

These are explained one by one in the following sections.

experimentInfo

This object contains metadata about the experiment: its name and id, its version number, when it is to be run and for how long, whether it recurs, whether it requires an additional layer of opt-in, and the URLs for further information about it. It should contain the following fields:

  • startDate: if null, then the experiment will start automatically as soon as it is installed. If you want to instead schedule a date in the future for the test to start, instead set it to a parseable date string.
  • duration: an integer, giving the number of days the test is to run for. E.g. if duration is 14 then the test will run from startDate to startDate + 2 weeks. Optional; duration defaults to 7 days if not specified.
  • testName: The human readable name of the experiment, as it will be displayed in menus and status pages, etc. Required.
  • testId: Can be either a number or a string, but must be unique - an experiment must not have the same id as another experiment or survey. These IDs will end up used in preference names, so it must not contain spaces. Required.
  • testInfoUrl: the URL of a page where the user can go to get more information about the experiment.
  • optInRequired: Set this to true if the test should not start automatically on the start date, but should instead only start if the user explicitly tells it to start. This should be the case for tests that change the Firefox UI, such as A-B tests. Note that the UI to support optInRequired has not yet been implemented in the client as of version 1.0.
  • recursAutomatically: Set this to true if the test should recur automatically after it ends.
  • recurrenceInterval: If recursAutomatically is true, then this is the number of days (measured from the startDate) after which the study will begin again. For obvious reasons, this must be longer than duration. Has no effect if recursAutomatically is false.
  • versionNumber: An integer for telling apart different versions of the same experiment. Whenever a significant change is made to the experiment code, the versionNumber should be incremented. The version number gets attached to the user's data upload when they submit results for the experiment; it can then be detected on the server side to tell apart submissions from different versions of the experiment. Required.
  • minTPVersion: A version string (e.g. "1.0a1") corresponding to a version of the Test Pilot extension. Versions of the extension older than minTPVersion will not attempt to download or run this study. It is optional; if not specified, all versions of the extension will download and run the study.


experimentInfo Example

The experimentInfo object for the Tab open/close study is as follows:

exports.experimentInfo = {
  startDate: null,
  duration: 7,
  testName: "Tab Open/Close Study",
  testId: 1,
  testInfoUrl: "https://testpilot.mozillalabs.com/testcases/tab-open-close.html",
  testResultsUrl: "https://testpilot.mozillalabs.com/testcases/tab-open-close/results.html",
  optInRequired: false,
  recursAutomatically: false,
  recurrenceInterval: 0,
  versionNumber: 2
};

dataStoreInfo

dataStoreInfo provides parameters for the initialization of a client-side SQLite database table which will be used to store the events that your experiment records. It's basically a schema description, that is fed to an object-relational mapper built into the extension.

When the Test Pilot extension runs your experiment, it will generate a Store object based on the specification you gave it in dataStoreInfo. Your observers (see below) will be able to call a method to record objects into this store. The info you provide in dataStoreInfo determines how the properties of this event object get mapped to the columns in the database.

dataStoreInfo must have the following properties (all required):

  • fileName: The name of the file where the SQLite database should be kept (inside the user's profile directory). It should be as descriptive as possible and end with ".sqlite".
  • tableName: The name of the database table to create inside this file. It should be descriptive and must not have spaces.
  • columns: An array of objects, each describing a single database column. Each column corresponds to one property on the event objects that you plan to pass in to storeEvent(). The properties that each column object must specify are as follows:
    • property: What property of the event object will be mapped to this column. For example, if you plan to pass in event objects that have a .timestamp property, then you will want a column object with property: "timestamp".
    • type: A code specifying the data storage type of the column. Not all data types are supported by the client yet. (TODO: explain this better)
    • displayName: The human-readable name that will be used to label this column whenever we display database contents. Can have spaces. Should be Title Cased.
    • displayValue (optional): If not provided, then the raw value that is stored will be what is displayed whenever we display database contents. If it is provided, it tells the client how to display the contents of this column. It must be either an array of strings, or a function that returns a string. If it's a function, the raw value will be passed into the function and the return value will be what's displayed. If it's an array, the raw value will be used as an index into the array, and the string at that index will be what's displayed (useful for enumerated type codes).
    • default (not yet implemented): What value to use if the eventObject passed in does not have a property matching this column.

Example of dataStoreInfo

exports.dataStoreInfo = {
  fileName: "testpilot_tabs_experiment_results.sqlite",
  tableName: "testpilot_tabs_experiment",
  columns: [
    {property: "event_code", type: TYPE_INT_32, displayName: "Event",
     displayValue: ["", "Open", "Close", "Drag", "Drop", "Switch", "Load",
                    "Startup","Shutdown", "Window Open", "Window Close"]},
    {property: "tab_position", type: TYPE_INT_32, displayName: "Tab Pos."},
    {property: "tab_window", type: TYPE_INT_32, displayName: "Window ID"},
    {property: "ui_method", type: TYPE_INT_32, displayName: "UI Method",
     displayValue: ["", "Click", "Keyboard", "Menu", "Link", "URL Entry", 
                    "Search", "Bookmark", "History"]},
    {property: "tab_site_hash", type: TYPE_INT_32, displayName: "Tab Group ID"},
    {property: "num_tabs", type: TYPE_INT_32, displayName: "Num. Tabs"},
    {property: "timestamp", type: TYPE_DOUBLE, displayName: "Time",
     displayValue: function(value) {return new Date(value).toLocaleString();}}
 ]};

Example of storing event objects

In observer code:

this.record({
  event_code: TabsExperimentConstants.DRAG_EVENT,
  timestamp: Date.now(),
  tab_position: index,
  num_tabs: event.target.parentNode.itemCount,
  ui_method: TabsExperimentConstants.UI_CLICK,
  tab_window: windowId
});

This anonymous event object passed to storeEvent has properties matching the properties of the columns specified in dataStoreInfo; it provides all the expected properties except for tab_site_hash, which will therefore default to 0.

webContent

(TODO out of date)

The webContent object is used to fill in the content of the experiment status page, which lives on the client side as part of the extension. It's at the url chrome://testpilot/content/status.html . webContent must contain the following properties:

  • inProgressHtml: A string containing a chunk of html that will be slotted into the status page while the test in in progress.
  • completedHtml: A string containing a different chunk of html that will be slotted into the status page after the test is complete.
  • upcomingHtml: A string containing a different chunk of html that will be slotted into the status page before the test begins (not yet implemented on the client).
  • onPageLoad: A function, that will be called when the status page has loaded. This can do whatever the experiment needs to do to finish rendering the page. For example, it can pull data from the data store and draw graphs on canvas elements.

The onPageLoad function will be called with the following arguments:

onPageLoad(experiment, document, graphUtils);

The values of these arguments are as follows:

  • experiment: reference the 'live' experiment object itself. This reference can be used to get the contents of the data store (as CSV or as JSON), to check the experiment status, etc. etc. TODO: document public API of experiment object
  • document: the status page document, the one you are running in. Because this code is in a securable-module context, the document reference would not otherwise be available. It's provided in case you want to do "document.getElementById" or whatever.
  • graphUtils: a simple graphing library object containing methods drawTimeSeriesGraph() and drawPieChart(). TODO: Document how to use these methods.

Observer

(TODO WAAAY out of date)

The Observer of your experiment has the very important responsibility of actually doing the data collection, and passing the collected data along to the store you defined in dataStoreInfo.

Observer must be a constructor function, a.k.a. a "class". It will be called like this:

new yourExperiment.Observer(window, store);

One Observer is instantiated for each window that is opened in the user's browser. A reference to that window is passed in as the first argument to the constructor function.

The second argument to the constructor function is a reference to the single data store object. (No matter how many Observers are instantiated, they all get a reference to the same dataStore instance.) See the dataStoreInfo section above. Your Observer can call .storeEvent() on this store object to save events to the database.

The Observer has chrome privileges, so it has free reign to use XPCOM components and so on to do the needed observations.

Besides the constructor, your Observer must provide an uninstall() method, which is called (with no arguments) when the observer is no longer needed. Any cleanup that you need to do (such as unregistering listeners, etc.) must be done in the uninstall() method.

Example Observer

Here's the skeleton of an Observer:

exports.Observer = function MyExperimentObserver(window, store) {
  this._init(window, store);
};
exports.Observer.prototype = {
  _init: function MyExperimentObserver__init(window, store) {
    console.info("Initializing a new MyExperimentObserver.");
    this._window = window;
    this._dataStore = store;
    // Install desired listeners, event handlers, etc. into the window here
    // e.g. register this._onClick as an event handler...
  },
  uninstall: function MyExperimentObserver_uninstall() {
    console.info("Uninstalling a MyExperimentObserver.");
    // Unregister this._onClick and any other registered event listeners
    // from this._window
  },
  _onClick: function MyExperimentObserver_onClick(clickEvent) {
    var eventToRecord;
    // Process clickEvent, calculate the properties of the event you want
    // to record, then write it to the data store:
    console.info("Recording a click event.");
    this._dataStore.storeEvent(eventToRecord);
  }
};