gregglind / micropilot

Lightweight event monitoring and observation in Firefox addons.
http://gregglind.github.com/micropilot/
4 stars 2 forks source link

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?

What is a topic?

Watch vs. Record

Why so much emphasis on the observer-service?

Timestamps on events?

Run indefinitely / forever

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

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

    Fuse({start: Date.now(),duration:1000 /* 1 sec */}).then(
     function(){Micropilot('mystudy').start()} )

Wait before running / delay startup (over restarts)?

  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)

Respect user privacy and private mode

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

  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?

Fusssing with internals:

Do studies persist after Firefox shutdown / restart?

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.

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

How do uploads work?

  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?

Use With Existing Test Pilot 1?


  require('timers').setInterval()

Event Entry Order is Wrong / Some got lost

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

I Want a Pony

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