Observe, record, and upload user events directly in your Firefox addon.
Philosophy
- Data operations are asynchronous
- Use promise-style apis.
- volo / git-submodule installable
- 100% code and test coverage.
// 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();
}
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)
- Create monitor (creates IndexedDb collections to store records)
- 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)
- monitoring
- directly, by calling
- Upload recorded data, to POST url of your choosing, including user metadata.
- Clean up (or don't!)
http://gregglind.github.com/micropilot/
- method: https://gist.github.com/3900355
- result: http://gregglind.github.com/micropilot/coverreport.html
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();
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()
}
- any jsonable (as claimed by
JSON.stringify
) object.
- just a string defining the 'message name' or 'message type'.
- (convention comes from the [observer-service]https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/deprecated/observer-service.html)
- you decide these for your own convenience.
watch
records {"msg": topic, "data": data, "ts": Date.now()}record
is unvarnished, "as is" recording.
- 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!)
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.
micropilot('yourid').lifetime() // will never resolve.
micropilot('yourid').start() // will never resolve.
- do it yourself... using
setTimeout
orFuse
like:
Fuse({start: Date.now(),duration:1000 /* 1 sec */}).then(
function(){Micropilot('mystudy').start()} )
- do it yourself... using
setTimeout
orFuse
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()} )
yourstudy.stop()
yourstudy.willrecord = false
-
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
- global
yourstudy.watch(more_topics_list)
yourstudy.unwatch(topics_list)
yourstudy.unwatch()
yourstudy.record(data)
- set these two prefs (issue report)[#6]
require("simple-prefs").prefs["micropilotlog"] = true
require("simple-prefs").prefs["sdk.console.logLevel"] = 0
yourstudy.stop();
- used as
IndexedDb
collection name. - used for the 'start time' persistent storage key, to persist between runs.
id
: don't change this
- Yes, in that the start time is recorded using
simple-storage
, ensuring that the duration is 'total duration'. In other wordslifetime(duration=many_ms)
will Do The Right Thing. - Data persists between runs (in the IndexedDb)
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
})
yourstudy._watchfn = function(evt){}
before any registration / start / lifetime.- (note: you can't just replace
watch
because it's aheritage
frozen object key)
- Write your own
- use Test Pilot 2, or similar.
- 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. */ })
// will stop the study run callback, if it exists
mystudy.startdate = whenever // setter
mystudy.lifetime(newduration).then(callback)
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!
})
- 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.
- 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()
- Events are written asynchronously. Order is not guaranteed.
- During catastrophic Firefox crash, some events may not be written.
micropilot('mystudy')._config.YOURKEY // persists in addon
- use
simple-storage
directly - store things in prefs, using
simple-prefs
orpreferences-service
- Make an IndexedDb or Sqlite db
- write a file to the profile
- Ponies are scheduled for Version 2.
- You can't have a pony, since this is JavaScript and not Python.
topic
:- [Addon-sdk observer-service]https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/deprecated/observer-service.html
- [nsIObserverService]https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIObserverService
- [Partial List of Firefox Global Topics]https://developer.mozilla.org/en-US/docs/Observer_Notifications
- the SDK calls these
types
, see system/events
observe
/notify
: GlobalobserverService
terms.watch
/unwatch
:Micropilot
listens toobserver
fortopics
, (fancifies them)[#5]record
: attempt to write data (undecorated, as is!) to theIndexedDb
event
: in Micropilot, anyJSON.stringify
-ableObject
. Used broadly for "a thing of human interest that happened", not in the strict JS sense.
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.
Gregg Lind [email protected] Ilana Segall [email protected]
David Keeler [email protected]
MPL2