/micropilot

Lightweight event monitoring and observation in Firefox addons.

Primary LanguageJavaScript

Micropilot

Observe, record, and upload user events directly in your Firefox addon.

Philosophy

Examples

// record {a:1, ts: <now>}.  Then upload.
require('micropilot').Micropilot('simplestudyid').start().
   record({a:1,ts: Date.now()}).then(function(m) m.ezupload())
   // which actually uploads!
// for 1 day, record *and annotate* any data notified on Observer topics ['topic1', 'topic2']
// then upload to <url>, after that 24 hour Fuse completes
require("micropilot").Micropilot('otherstudyid').start().watch(['topic1','topic2']).
  lifetime(24 * 60 * 60 * 1000 /*1 day Fuse */).then(
    function(mtp){ mtp.upload(url); mtp.stop() })
let monitor_tabopen = require('micropilot').Micropilot('tapopenstudy').start();
var tabs = require('tabs');
tabs.on('ready', function () {
  monitor_tabopen.record({'msg:' 'tab ready', 'ts': Date.now()})
});

monitor_tabopen.lifetime(86400*1000).then(function(mon){mon.ezupload()});
  // Fuse:  24 hours-ish after first start, upload

if (user_tells_us_to_stop_snooping){
  monitor_tabopen.stop();
}

Install

  npm install -g volo

  mkdir myaddon
  cd myaddon
  cfx init # the addon
  mkdir packages
  # augment package.json -> "dependencies" ["micropilot"]

  volo add micropilot packages/micropilot  # or git submodule

  # edit lib/main.js
  require("simple-prefs").prefs["micropilotlog"] = true;
  let mtp=require('micropilot').Micropilot("astudy").start().record({a:1}).then(console.log)

Overview

  1. Create monitor (creates IndexedDb collections to store records)
  2. Record JSON-able data
    • directly, by calling record(), if the monitor is in scope.
    • indirectly, by
      • monitoring observer-service topics
      • require("observer-service").notify(topic,data)
  3. Upload recorded data, to POST url of your choosing, including user metadata.
  4. Clean up (or don't!)

Api

http://gregglind.github.com/micropilot/

Coverage and Tests

Longer, Annotated Example, Demoing Api

  let micropilot = require("micropilot");
  let monitor = require("micropilot").Micropilot('tabsmonitor');
  /* Effects:
    * Create IndexedDb:  youraddonid:micropilot-tabsmonitor
    * Create objectStore: tabsmonitor
    * Using `simple-store` persist the startdate of `tabsmonitor`
      as now.
    *
  */
  monitor.record({c:1}).then(function(d){
    assert.deepEqual(d,{"id":1,"data":{"c":1}} ) })
  /* in db => {"c"1, "eventstoreid":1} <- added "eventstoreid" key */
  /* direct record call.  Simplest API. */

  monitor.data().then(function(data){assert.ok(data.length==1)})
  /* `data()` promises this data:  [{"c":1, "eventstoreid":1}] */

  monitor.clear().then(function(){assert.pass("async, clear the data and db")})

  // *Observe using topic channels*

  monitor.watch(['topic1','topic2'])
  /* Any observer-service.notify events in 'topic1', 'topic2' will be
     recorded in the IndexedDb */

  monitor.watch(['topic3']) /* add topic3 as well */

  monitor.unwatch(['topic3']) /* changed our mind. */

  observer.notify('kitten',{ts: Date.now(), a:1}) // not recorded, wrong topic

  observer.notify('topic1',{ts: Date.now(), b:1}) // will be recorded, good topic

  monitor.data().then(function(data){/* console.log(JSON.stringify(data))*/ })
  /* [{"ts": somets, "b":1}] */

  monitor.stop().record({stopped:true})  // won't record

  monitor.data().then(function(data){
    assert.ok(data.length==1);
    assert.ok(data[0]['b'] == 1);
  })

  monitor.willrecord = true;  // turns recording back on.

  // Longer runs
  let microsecondstorun = 86400 * 1000 // 1 day!
  monitor.lifetime(microsecondstorun).then(function(mtp){
    console.log("Promises a Fuse that will be");
    console.log("called no earlier 24 hours after mtp.startdate.");
    console.log("Even / especially surviving Firefox restarts.");
    console.log("`lifetime` or `stop` stops any previous fuses.");
    mtp.stop(); /* stop this study from recording*/
    mtp.upload(UPLOAD_URL).then(function(response){
      if (! micropilot.GOODSTATUS[response.status]){
        console.error("what a bummer.")
      }
    })
  });

  monitor.stop();  // stop the Fuse!
  monitor.lifetime();   // no argument -> forever.  Returned promise will never resolve.

  // see what will be sent.
  monitor.upload('http://fake.com',{simulate: true}).then(function(request){
    /*
    console.log(JSON.stringify(request.content));

    {"events":[{"ts":1356989653822,"b":1,"eventstoreid":1}],
    "userdata":{"appname":"Firefox",
      "location":"en-US",
      "fxVersion":"17.0.1",
      "updateChannel":"release",
      "addons":[]},
    "ts":1356989656951,
    "uploadid":"5d772ebd-1086-ea46-8439-0979217d29f7",
    "personid":"57eef97d-c14b-6840-b966-b01e1f6eb04c"}
    */
  })

  /* we have overrides for some pieces if we need them...*/
  monitor._config.personid /* store/modify the person uuid between runs */
  monitor.startdate /* setting this stops the Fuse, to allow 're-timing' */
  monitor.upload('fake.com',{simulate:true, uploadid: 1}); /* give an uploadid */

  monitor.stop();
  assert.pass();

Supported Versions

Desktop Firefox 17+ is supported. (16's IndexedDB is too different). Firefox 17 needslib/indexed-db-17.js. 18+ doesn't require this.

Mobile Firefox 21+ is known to work. Other versions are untested, but probably safe.

Verifying version compatability is your responsibility.

  if (require('sdk/system/xul-app').version < 17){
    require("request").Request("personalized/phone/home/url").get()
  }

FAQ

What are events?

  • any jsonable (as claimed by JSON.stringify) object.

What is a topic?

Watch vs. Record

  • watch records {"msg": topic, "data": data, "ts": Date.now()}
  • record is unvarnished, "as is" recording.

Why so much emphasis on the observer-service?

  • global message passing mechanism that crosses sandboxes (allows inter-addon communication)
  • robust and well-tested
  • many 'interesting' events are already being logged there.
  • (remember, you can record directly, if the monitor is in scope!)

Timestamps on events?

  • record - you need to timestamp your own events!
  • watch - will come in with the timestamp at recording. This might be different than when the event actually originated.

Run indefinitely / forever

micropilot('yourid').lifetime() // will never resolve. micropilot('yourid').start() // will never resolve.

Wait before running / delay startup (for this restart)?

  • do it yourself... using setTimeout or Fuse like:
	Fuse({start: Date.now(),duration:1000 /* 1 sec */}).then(
	 function(){Micropilot('mystudy').start()} )

Wait before running / delay startup (over restarts)?

  • do it yourself... using setTimeout or Fuse like:
  let {storage} = require("simple-storage");
  if (! storage.firststart) storage.firststart = Date.now(); // tied to addon
  Fuse({start: storage.firststart,duration:86400 * 7 * 1000 /* 7 days */}).then(
   function(){ Micropilot('delayedstudy').start()} )

Stop recording (messages arrive but aren't recorded)

  • yourstudy.stop()
  • yourstudy.willrecord = false

Respect user privacy and private mode

  • Given the changes in require("private-browsing") at Firefox 20, this is really up to study authors to track themselves. Ask @gregglind if you need help.

  • Be extra wary of globalObserver notifications, which might come from private windows.

  • Changes at Firefox 20:

    • global isActive disappears
    • per-window private mode starts

Add more topics (channels), or remove them:

  yourstudy.watch(more_topics_list)
  yourstudy.unwatch(topics_list)

Remove all topics

yourstudy.unwatch()

Just record some event without setting up a topic:

yourstudy.record(data)

See / log all recording events in the console

  • set these two prefs (issue report)[#6]
  require("simple-prefs").prefs["micropilotlog"] = true
  require("simple-prefs").prefs["sdk.console.logLevel"] = 0

Stop the callback in lifetime(duration).then()... (unlight the Fuse!)

yourstudy.stop();

Why have a studyid?

  • used as IndexedDb collection name.
  • used for the 'start time' persistent storage key, to persist between runs.

Fusssing with internals:

  • id: don't change this

Do studies persist after Firefox shutdown / restart?

  • Yes, in that the start time is recorded using simple-storage, ensuring that the duration is 'total duration'. In other words lifetime(duration=many_ms) will Do The Right Thing.
  • Data persists between runs (in the IndexedDb)

How do I clean up my mess?

  Micropilot('studyname').lifetime(duration).then(function(mtp){
    mtp.stop();
    mtp.upload(somewhere);
    mtp.cleardata();    // collection name might still exist
    require('simple-storage').store.micropilot = undefined
    let addonid = require('self').id;
    require("sdk/addon/installer").uninstall(addonid); // apoptosis of addon
  })

I don't want to actually record / I want to do something else on observation.

  • yourstudy._watchfn = function(evt){} before any registration / start / lifetime.
  • (note: you can't just replace watch because it's a heritage frozen object key)

How can I notify people on start / stop / comletion / upload?

  • Write your own
  • use Test Pilot 2, or similar.

How do uploads work?

  • snoops some user data, and all recorded events
  • to url. returns promise on response.
  • for now, it's up to you to check that response, url and otherwise check that you are happy with it.
  let micro = require('micropilot');
  let studyname = 'mystudy';
  micro.Micropilot(studyname).upload(micro.UPLOAD_URL + studyname).then(
    function(response){ /* check response, retry using Fuse, etc. */ })

My startdate is wrong

  // will stop the study run callback, if it exists
  mystudy.startdate = whenever  // setter
  mystudy.lifetime(newduration).then(callback)

Recurring upload of data?

  let {storage} = require("simple-storage");
  if (! storage.lastupload) storage.lastupload = Date.now(); // tied to addon
  let mtp = Micropilot('mystudy');  // running, able to record.

  Fuse({start: storage.lastupload,duration:86400 * 1000 /* 1 days */}).then(
    function(){
      storage.lastupload = Date.now();
      mtp.upload(URL).then(mtp.clear);  // if you really want clearing between!
    })

How well does it perform / scale?

  • on mobile it has been measured to write 40 events/sec.
  • on desktop (OSX) it has been measured at 120-240 events/sec.
  • Plan for 10, and you will probably be happier.

Use With Existing Test Pilot 1?

  • create a Test Pilot experiment jar that loads your addon (using study_base_classes).
  • make the addon self destructing, using a Fuse and addonManager.
  • (the study here will track its own data and uploads)
  • Downsides
    • TP1 will lose control of being able to stop the study
    • much less transparent where the data is being stored.

  require('timers').setInterval()

Event Entry Order is Wrong / Some got lost

  • Events are written asynchronously. Order is not guaranteed.
  • During catastrophic Firefox crash, some events may not be written.

I want to persist other aspects / attributes of the study across restarts

  • micropilot('mystudy')._config.YOURKEY // persists in addon
  • use simple-storage directly
  • store things in prefs, using simple-prefs or preferences-service
  • Make an IndexedDb or Sqlite db
  • write a file to the profile

I Want a Pony

  • Ponies are scheduled for Version 2.
  • You can't have a pony, since this is JavaScript and not Python.

Glossary

Other Gory Details and Sharp Edges:

Study lifetime(duration).then(callback) is a setTimout based on Date.now(), startdate and the duration. If you want a more sophisticated timing loop, use a Fuse or write your own.

Authors

Gregg Lind glind@mozilla.com Ilana Segall isegall@mozilla.com

Contributors

David Keeler dkeeler@mozilla.com

License

MPL2