Skip to content

Instantly share code, notes, and snippets.

@samueleiche
Created November 29, 2019 09:15
Show Gist options
  • Save samueleiche/8d4776ec19ad094790bc91c90da33992 to your computer and use it in GitHub Desktop.
Save samueleiche/8d4776ec19ad094790bc91c90da33992 to your computer and use it in GitHub Desktop.
Experimental user action tracker
/**
* 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