Created
November 29, 2019 09:15
-
-
Save samueleiche/8d4776ec19ad094790bc91c90da33992 to your computer and use it in GitHub Desktop.
Experimental user action tracker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The tracker object's instance class. | |
* | |
* @param {Object} config | |
*/ | |
class TrackerNode { | |
constructor(config) { | |
/** | |
* The unique ID of the tracker node. | |
*/ | |
this.id = config.id; | |
/** | |
* The global storage namespace for all trackers. | |
*/ | |
this.namespace = '__tracker__'; | |
/** | |
* The initial resolved tracker state. | |
*/ | |
this.context = this._resolveContext(config); | |
} | |
/** | |
* Sends an event as an `action` to update the corresponding record. | |
* If a new value is not sent, the record's count will be increased instead. | |
* | |
* @param {string} action - The record's action. | |
* @param {*} [value] - The new value for the record. | |
*/ | |
send(action, newValue = null) { | |
const { records } = this.context; | |
const calledRecord = records.find((record) => record.action === action); | |
const calledIndex = records.indexOf(calledRecord); | |
if (calledIndex === -1) return; | |
const newRecord = Object.assign( | |
{}, | |
calledRecord, | |
{ | |
history: this._mapHistory(calledRecord), | |
timestamp: Date.now(), | |
} | |
); | |
if (newValue && newRecord.hasOwnProperty('value')) { | |
newRecord.value = newValue; | |
} else { | |
newRecord.count++; | |
} | |
records[calledIndex] = newRecord; | |
this._toCache(); | |
} | |
suggestAction() { | |
// TODO: Return record of largest weight | |
} | |
// TODO: get sorted records by weight | |
/** | |
* Resolves the given `context` relative to this node. | |
* | |
* @param {Object} config | |
*/ | |
_resolveContext(config) { | |
const cachedContext = this._resolveCache().find((ctx) => ctx.id === this.id); | |
const records = this._mapRecords(config.records); | |
const meta = config.meta; | |
return cachedContext | |
? cachedContext | |
: { | |
id: this.id, | |
meta, | |
records, | |
}; | |
} | |
_mapHistory(record) { | |
const history = {}; | |
const historyProps = ['timestamp', 'count', 'value']; | |
historyProps.forEach((key) => { | |
if (record.hasOwnProperty(key)) { | |
history[key] = record[key]; | |
} | |
}); | |
return history; | |
} | |
_mapRecords(initialRecords) { | |
const records = []; | |
initialRecords.forEach((record) => { | |
if (record.hasOwnProperty('count') || record.hasOwnProperty('value')) { | |
records.push({ | |
...record, | |
history: {}, | |
timestamp: Date.now(), | |
}); | |
} else { | |
throw new Error(`Missing "count" or "value" in record: ${record}`); | |
} | |
}); | |
return records; | |
} | |
/** | |
* Returns the collection of trackers in the cache, empty array if none cached. | |
*/ | |
_getCache() { | |
return JSON.parse(localStorage.getItem(this.namespace)) || []; | |
} | |
/** | |
* Returns the cache value and sets the localStorage namespace if not already set. | |
*/ | |
_resolveCache() { | |
const cache = this._getCache(); | |
if (!cache.length) { | |
localStorage.setItem(this.namespace, JSON.stringify(cache)); | |
} | |
return cache; | |
} | |
/** | |
* Update the tracker in cache, add if it doesn't exist. | |
*/ | |
_toCache() { | |
const cache = this._getCache(); | |
const recordIndex = cache.findIndex((record) => record.id === this.id); | |
if (recordIndex === -1) { | |
cache.push(this.context); | |
} else { | |
cache[recordIndex] = this.context; | |
} | |
try { | |
localStorage.setItem(this.namespace, JSON.stringify(cache)); | |
} catch (e) { | |
throw new Error(e); | |
} | |
} | |
} | |
/** | |
* The tracker's factory function. | |
* | |
* @param {Object} config - The initial tracker state or `context` for the tracker. | |
* @param {string} config.id - The unique ID of the tracker node. | |
* @param {Object[]} config.records - The records to track. | |
* @param {Object} [config.meta] - Thetracker's meta values. | |
* | |
* @example | |
* const routeTracker = createTracker({ | |
* id: 'route', | |
* records: [ | |
* { name: 'login', count: 0, action: 'DO_LOGIN' }, | |
* { name: 'register', count: 0, action: 'DO_REGISTER' }, | |
* // ... | |
* ], | |
* }); | |
* | |
* routeTracker.send('DO_LOGIN'); | |
*/ | |
function createTracker(config) { | |
const resolvedConfig = Object.assign( | |
{ | |
id: '(tracker)', | |
meta: {}, | |
records: [], | |
}, | |
config, | |
); | |
return new TrackerNode(resolvedConfig); | |
} | |
const feedbackConfig = { | |
id: 'feedbackTracker', | |
records: [ | |
{ name: 'good', count: 0, action: 'CLICK_GOOD' }, | |
{ name: 'bad', count: 0, action: 'CLICK_BAD' }, | |
{ name: 'close', count: 0, action: 'CLICK_CLOSE' }, | |
], | |
}; | |
const feedbackTracker = createTracker(feedbackConfig); | |
feedbackTracker.send('CLICK_GOOD'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment