1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/6f8070735e6f/ Changeset: 6f8070735e6f User: carlfeberhard Date: 2014-04-21 17:37:26 Summary: Metrics: make the cache use localStorage for persistence Affected #: 3 files diff -r f1c9df0b5cabb998dd82dc6a154f66677173ffb8 -r 6f8070735e6f36fd1cd7b79d7cef1d69ebf8c1d6 static/scripts/packed/utils/metrics-logger.js --- a/static/scripts/packed/utils/metrics-logger.js +++ b/static/scripts/packed/utils/metrics-logger.js @@ -1,1 +1,1 @@ -define([],function(){function h(z){z=z||{};var y=this;y.userId=window.bootstrapped?window.bootstrapped.user.id:null;y.userId=y.userId||z.userId||null;y.consoleLogger=z.consoleLogger||null;y._init(z);return y}h.ALL=0;h.DEBUG=10;h.INFO=20;h.WARN=30;h.ERROR=40;h.METRIC=50;h.NONE=100;h.defaultOptions={logLevel:h.NONE,consoleLevel:h.NONE,defaultNamespace:"Galaxy",clientPrefix:"client.",maxCacheSize:3000,postSize:1000,addTime:true,postUrl:"/api/metrics",getPingData:undefined,onServerResponse:undefined};h.prototype._init=function i(A){var z=this;z.options={};for(var y in h.defaultOptions){if(h.defaultOptions.hasOwnProperty(y)){z.options[y]=(A.hasOwnProperty(y))?(A[y]):(h.defaultOptions[y])}}z.options.logLevel=z._parseLevel(z.options.logLevel);z.options.consoleLevel=z._parseLevel(z.options.consoleLevel);z._sending=false;z._postSize=z.options.postSize;z._initCache();return z};h.prototype._initCache=function a(){this.cache=new w({maxSize:this.options.maxCacheSize})};h.prototype._parseLevel=function j(A){var z=typeof A;if(z==="number"){return A}if(z==="string"){var y=A.toUpperCase();if(h.hasOwnProperty(y)){return h[y]}}throw new Error("Unknown log level: "+A)};h.prototype.emit=function m(B,A,z){var y=this;A=A||y.options.defaultNamespace;if(!B||!z){return y}B=y._parseLevel(B);if(B>=y.options.logLevel){y._addToCache(B,A,z)}if(y.consoleLogger&&B>=y.options.consoleLevel){y._emitToConsole(B,A,z)}return y};h.prototype._addToCache=function b(D,A,z){this._emitToConsole("debug","MetricsLogger",["_addToCache:",arguments,this.options.addTime,this.cache.length()]);var y=this;try{var C=y.cache.add(y._buildEntry(D,A,z));if(C>=y._postSize){y._postCache()}}catch(B){y._emitToConsole("warn","MetricsLogger",["Metrics logger could not stringify logArguments:",A,z]);y._emitToConsole("error","MetricsLogger",[B])}return y};h.prototype._buildEntry=function r(B,z,y){this._emitToConsole("debug","MetricsLogger",["_buildEntry:",arguments]);var A={level:B,namespace:this.options.clientPrefix+z,args:y};if(this.options.addTime){A.time=new Date().toISOString()}return A};h.prototype._postCache=function s(B){B=B||{};this._emitToConsole("info","MetricsLogger",["_postCache",B,this._postSize]);if(!this.options.postUrl||this._sending){return jQuery.when({})}var A=this,D=B.count||A._postSize,y=A.cache.get(D),C=y.length,z=(typeof A.options.getPingData==="function")?(A.options.getPingData()):({});z.metrics=A._preprocessCache(y);A._sending=true;return jQuery.post(A.options.postUrl,z).always(function(){A._sending=false}).fail(function(){A._postSize=A.options.maxCacheSize}).done(function(E){if(typeof A.options.onServerResponse==="function"){A.options.onServerResponse(E)}A.cache.remove(C);A._postSize=A.options.postSize})};h.prototype._preprocessCache=function f(y){return["[",(y.join(",\n")),"]"].join("\n")};h.prototype._emitToConsole=function c(C,B,A){var y=this;if(!y.consoleLogger){return y}var z=Array.prototype.slice.call(A,0);z.unshift(B);if(C>=h.METRIC&&typeof(y.consoleLogger.info)==="function"){return y.consoleLogger.info.apply(y.consoleLogger,z)}else{if(C>=h.ERROR&&typeof(y.consoleLogger.error)==="function"){return y.consoleLogger.error.apply(y.consoleLogger,z)}else{if(C>=h.WARN&&typeof(y.consoleLogger.warn)==="function"){y.consoleLogger.warn.apply(y.consoleLogger,z)}else{if(C>=h.INFO&&typeof(y.consoleLogger.info)==="function"){y.consoleLogger.info.apply(y.consoleLogger,z)}else{if(C>=h.DEBUG&&typeof(y.consoleLogger.debug)==="function"){y.consoleLogger.debug.apply(y.consoleLogger,z)}else{if(typeof(y.consoleLogger.log)==="function"){y.consoleLogger.log.apply(y.consoleLogger,z)}}}}}}return y};h.prototype.log=function g(){this.emit(1,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};h.prototype.debug=function l(){this.emit(h.DEBUG,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};h.prototype.info=function u(){this.emit(h.INFO,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};h.prototype.warn=function t(){this.emit(h.WARN,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};h.prototype.error=function p(){this.emit(h.ERROR,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};h.prototype.metric=function n(){this.emit(h.METRIC,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};function w(z){var y=this;y._cache=[];return y._init(z||{})}w.defaultOptions={maxSize:5000};w.prototype._init=function i(y){this.maxSize=y.maxSize||w.defaultOptions.maxSize;return this};w.prototype.add=function k(A){var z=this,y=(z.length()+1)-z.maxSize;if(y>0){z.remove(y)}z._cache.push(z._preprocessEntry(A));return z.length()};w.prototype._preprocessEntry=function q(y){return JSON.stringify(y)};w.prototype.length=function e(){return this._cache.length};w.prototype.get=function v(y){return this._cache.slice(0,y)};w.prototype.remove=function x(y){return this._cache.splice(0,y)};w.prototype.stringify=function o(y){return["[",(this.get(y).join(",\n")),"]"].join("\n")};w.prototype.print=function d(){this._cache.forEach(function(y){console.log(y)})};return{MetricsLogger:h,LoggingCache:w}}); \ No newline at end of file +define([],function(){function i(D){D=D||{};var C=this;C.userId=window.bootstrapped?window.bootstrapped.user.id:null;C.userId=C.userId||D.userId||null;C.consoleLogger=D.consoleLogger||null;C._init(D);return C}i.ALL=0;i.DEBUG=10;i.INFO=20;i.WARN=30;i.ERROR=40;i.METRIC=50;i.NONE=100;i.defaultOptions={logLevel:i.NONE,consoleLevel:i.NONE,defaultNamespace:"Galaxy",clientPrefix:"client.",maxCacheSize:3000,postSize:1000,addTime:true,cacheKeyPrefix:"logs-",postUrl:"/api/metrics",delayPostInMs:1000*60*10,getPingData:undefined,onServerResponse:undefined};i.prototype._init=function j(E){var D=this;D.options={};for(var C in i.defaultOptions){if(i.defaultOptions.hasOwnProperty(C)){D.options[C]=(E.hasOwnProperty(C))?(E[C]):(i.defaultOptions[C])}}D.options.logLevel=D._parseLevel(D.options.logLevel);D.options.consoleLevel=D._parseLevel(D.options.consoleLevel);D._sending=false;D._waiting=null;D._postSize=D.options.postSize;D._initCache();return D};i.prototype._initCache=function a(){try{this.cache=new z({maxSize:this.options.maxCacheSize,key:this.options.cacheKeyPrefix+this.userId})}catch(C){this._emitToConsole("warn","MetricsLogger",["Could not intitialize logging cache:",C]);this.options.logLevel=i.NONE}};i.prototype._parseLevel=function n(E){var D=typeof E;if(D==="number"){return E}if(D==="string"){var C=E.toUpperCase();if(i.hasOwnProperty(C)){return i[C]}}throw new Error("Unknown log level: "+E)};i.prototype.emit=function q(F,E,D){var C=this;E=E||C.options.defaultNamespace;if(!F||!D){return C}F=C._parseLevel(F);if(F>=C.options.logLevel){C._addToCache(F,E,D)}if(C.consoleLogger&&F>=C.options.consoleLevel){C._emitToConsole(F,E,D)}return C};i.prototype._addToCache=function b(H,E,D){this._emitToConsole("debug","MetricsLogger",["_addToCache:",arguments,this.options.addTime,this.cache.length()]);var C=this;try{var G=C.cache.add(C._buildEntry(H,E,D));if(G>=C._postSize){C._postCache()}}catch(F){C._emitToConsole("warn","MetricsLogger",["Metrics logger could not stringify logArguments:",E,D]);C._emitToConsole("error","MetricsLogger",[F])}return C};i.prototype._buildEntry=function u(F,D,C){this._emitToConsole("debug","MetricsLogger",["_buildEntry:",arguments]);var E={level:F,namespace:this.options.clientPrefix+D,args:C};if(this.options.addTime){E.time=new Date().toISOString()}return E};i.prototype._postCache=function v(F){F=F||{};this._emitToConsole("info","MetricsLogger",["_postCache",F,this._postSize]);if(!this.options.postUrl||this._sending){return jQuery.when({})}var E=this,H=F.count||E._postSize,C=E.cache.get(H),G=C.length,D=(typeof E.options.getPingData==="function")?(E.options.getPingData()):({});D.metrics=JSON.stringify(C);E._sending=true;return jQuery.post(E.options.postUrl,D).always(function(){E._sending=false}).fail(function(K,I,J){E._postSize=E.options.maxCacheSize;this.emit("error","MetricsLogger",["_postCache error:",K.readyState,K.status,K.responseJSON||K.responseText])}).done(function(I){if(typeof E.options.onServerResponse==="function"){E.options.onServerResponse(I)}E.cache.remove(G);E._postSize=E.options.postSize})};i.prototype._delayPost=function k(){var C=this;C._waiting=setTimeout(function(){C._waiting=null},C.options.delayPostInMs)};i.prototype._emitToConsole=function c(G,F,E){var C=this;if(!C.consoleLogger){return C}var D=Array.prototype.slice.call(E,0);D.unshift(F);if(G>=i.METRIC&&typeof(C.consoleLogger.info)==="function"){return C.consoleLogger.info.apply(C.consoleLogger,D)}else{if(G>=i.ERROR&&typeof(C.consoleLogger.error)==="function"){return C.consoleLogger.error.apply(C.consoleLogger,D)}else{if(G>=i.WARN&&typeof(C.consoleLogger.warn)==="function"){C.consoleLogger.warn.apply(C.consoleLogger,D)}else{if(G>=i.INFO&&typeof(C.consoleLogger.info)==="function"){C.consoleLogger.info.apply(C.consoleLogger,D)}else{if(G>=i.DEBUG&&typeof(C.consoleLogger.debug)==="function"){C.consoleLogger.debug.apply(C.consoleLogger,D)}else{if(typeof(C.consoleLogger.log)==="function"){C.consoleLogger.log.apply(C.consoleLogger,D)}}}}}}return C};i.prototype.log=function h(){this.emit(1,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};i.prototype.debug=function p(){this.emit(i.DEBUG,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};i.prototype.info=function x(){this.emit(i.INFO,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};i.prototype.warn=function w(){this.emit(i.WARN,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};i.prototype.error=function t(){this.emit(i.ERROR,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};i.prototype.metric=function r(){this.emit(i.METRIC,this.options.defaultNamespace,Array.prototype.slice.call(arguments,0))};function z(D){var C=this;return C._init(D||{})}z.defaultOptions={maxSize:5000};z.prototype._init=function j(C){if(!this._hasStorage()){throw new Error("LoggingCache needs localStorage")}if(!C.key){throw new Error("LoggingCache needs key for localStorage")}this.key=C.key;this._initStorage();this.maxSize=C.maxSize||z.defaultOptions.maxSize;return this};z.prototype._hasStorage=function A(){var D="test";try{localStorage.setItem(D,D);localStorage.removeItem(D);return true}catch(C){return false}};z.prototype._initStorage=function m(){if(localStorage.getItem(this.key)===null){return this.empty()}return this};z.prototype.add=function o(E){var D=this,F=D._fetchAndParse(),C=(F.length+1)-D.maxSize;if(C>0){F.splice(0,C)}F.push(E);D._unparseAndStore(F);return F.length};z.prototype._fetchAndParse=function g(){var C=this;return JSON.parse(localStorage.getItem(C.key))};z.prototype._unparseAndStore=function f(C){var D=this;return localStorage.setItem(D.key,JSON.stringify(C))};z.prototype.length=function e(){return this._fetchAndParse().length};z.prototype.get=function y(C){return this._fetchAndParse().slice(0,C)};z.prototype.remove=function B(C){var E=this._fetchAndParse(),D=E.splice(0,C);this._unparseAndStore(E);return D};z.prototype.empty=function l(){localStorage.setItem(this.key,"[]");return this};z.prototype.stringify=function s(C){return JSON.stringify(this.get(C))};z.prototype.print=function d(){console.log(JSON.stringify(this._fetchAndParse(),null," "))};return{MetricsLogger:i,LoggingCache:z}}); \ No newline at end of file diff -r f1c9df0b5cabb998dd82dc6a154f66677173ffb8 -r 6f8070735e6f36fd1cd7b79d7cef1d69ebf8c1d6 static/scripts/utils/metrics-logger.js --- a/static/scripts/utils/metrics-logger.js +++ b/static/scripts/utils/metrics-logger.js @@ -3,8 +3,10 @@ /*global window, jQuery, console */ /*============================================================================= TODO: - broken pipe due to onunload post in webkit, safari - need to use persistence + while anon: logs saved to 'logs-null' - this will never post + unless we manually do so at/after login + OR prepend when userId and localStorage has 'logs-null' + wire up _delayPost and test =============================================================================*/ /** @class MetricsLogger @@ -31,8 +33,7 @@ options = options || {}; var self = this; - //TODO: this might be used if we store the logs in browser storage - ///** */ + ///** get the current user's id from bootstrapped data or options */ self.userId = window.bootstrapped? window.bootstrapped.user.id: null; self.userId = self.userId || options.userId || null; @@ -73,9 +74,13 @@ postSize : 1000, /** T/F whether to add a timestamp to incoming cached messages */ addTime : true, + /** string to prefix to userid for cache web storage */ + cacheKeyPrefix : 'logs-', /** the relative url to post messages to */ postUrl : '/api/metrics', + /** delay before trying post again after two failures */ + delayPostInMs : 1000 * 60 * 10, /** an (optional) function that should return an object; used to send additional data with the metrics */ getPingData : undefined, @@ -97,8 +102,13 @@ self.options.consoleLevel = self._parseLevel( self.options.consoleLevel ); //self._emitToConsole( 'debug', 'MetricsLogger', 'MetricsLogger.options:', self.options ); + /** is the logger currently sending? */ self._sending = false; + /** the setTimeout id if the logger POST has failed more than once */ + self._waiting = null; + /** the current number of entries to send in a POST */ self._postSize = self.options.postSize; + self._initCache(); return self; @@ -106,8 +116,15 @@ /** initialize the cache */ MetricsLogger.prototype._initCache = function _initCache(){ - this.cache = new LoggingCache({ maxSize : this.options.maxCacheSize }); - + try { + this.cache = new LoggingCache({ + maxSize : this.options.maxCacheSize, + key : this.options.cacheKeyPrefix + this.userId + }); + } catch( err ){ + this._emitToConsole( 'warn', 'MetricsLogger', [ 'Could not intitialize logging cache:', err ] ); + this.options.logLevel = MetricsLogger.NONE; + } }; /** return the numeric log level if level in 'none, debug, log, info, warn, error' */ @@ -134,7 +151,7 @@ return self; } // add to cache if proper level -//TODO: respect do not track? + //TODO: respect do not track? //if( !navigator.doNotTrack && level >= self.options.logLevel ){ level = self._parseLevel( level ); if( level >= self.options.logLevel ){ @@ -192,7 +209,6 @@ MetricsLogger.prototype._postCache = function _postCache( options ){ options = options || {}; this._emitToConsole( 'info', 'MetricsLogger', [ '_postCache', options, this._postSize ]); -//TODO: remove jq dependence // short circuit if we're already sending if( !this.options.postUrl || this._sending ){ @@ -209,15 +225,22 @@ //console.debug( postSize, entriesLength ); // add the metrics and send - postData.metrics = self._preprocessCache( entries ); + postData.metrics = JSON.stringify( entries ); + //console.debug( postData.metrics ); self._sending = true; return jQuery.post( self.options.postUrl, postData ) .always( function(){ self._sending = false; }) - .fail( function(){ + .fail( function( xhr, status, message ){ // if we failed the previous time, set the next post target to the max num of entries self._postSize = self.options.maxCacheSize; +//TODO:?? + // log this failure to explain any gap in metrics + this.emit( 'error', 'MetricsLogger', [ '_postCache error:', + xhr.readyState, xhr.status, xhr.responseJSON || xhr.responseText ]); +//TODO: still doesn't solve the problem that when cache == max, post will be tried on every emit +//TODO: see _delayPost }) .done( function( response ){ if( typeof self.options.onServerResponse === 'function' ){ @@ -232,10 +255,13 @@ // return the xhr promise }; -/** Preprocess a number of cache entries for sending to the server (stringification) */ -MetricsLogger.prototype._preprocessCache = function _preprocessCache( entries ){ - return [ '[', ( entries.join( ',\n' ) ), ']' ].join( '\n' ); - //return [ '[', ( entries.join( ',' ) ), ']' ].join( '' ); +/** set _waiting to true and, after delayPostInMs, set it back to false */ +MetricsLogger.prototype._delayPost = function _delayPost(){ +//TODO: this won't work between pages + var self = this; + self._waiting = setTimeout( function(){ + self._waiting = null; + }, self.options.delayPostInMs ); }; @@ -306,7 +332,11 @@ }; -//============================================================================= +/* ============================================================================ +TODO: + need a performance pass - the JSON un/parsing is a bit much + +============================================================================ */ /** @class LoggingCache * Simple implementation of cache wrapping an array. * @@ -315,7 +345,6 @@ */ function LoggingCache( options ){ var self = this; - self._cache = []; return self._init( options || {} ); } @@ -327,52 +356,104 @@ /** initialize with options */ LoggingCache.prototype._init = function _init( options ){ + if( !this._hasStorage() ){ + //TODO: fall back to jstorage + throw new Error( 'LoggingCache needs localStorage' ); + } + if( !options.key ){ + throw new Error( 'LoggingCache needs key for localStorage' ); + } + this.key = options.key; + this._initStorage(); + this.maxSize = options.maxSize || LoggingCache.defaultOptions.maxSize; return this; }; +/** tests for localStorage fns */ +LoggingCache.prototype._hasStorage = function _hasStorage(){ +//TODO: modernizr + var test = 'test'; + try { + localStorage.setItem( test, test ); + localStorage.removeItem( test ); + return true; + } catch( e ){ + return false; + } +}; + +/** if no localStorage set for key, initialize to empty array */ +LoggingCache.prototype._initStorage = function _initStorage(){ + if( localStorage.getItem( this.key ) === null ){ + return this.empty(); + } + return this; +}; + /** add an entry to the cache, removing the oldest beforehand if size >= maxSize */ LoggingCache.prototype.add = function add( entry ){ var self = this, - overage = ( self.length() + 1 ) - self.maxSize; + _cache = self._fetchAndParse(), + overage = ( _cache.length + 1 ) - self.maxSize; if( overage > 0 ){ - self.remove( overage ); + _cache.splice( 0, overage ); } - self._cache.push( self._preprocessEntry( entry ) ); - return self.length(); + _cache.push( entry ); + self._unparseAndStore( _cache ); + return _cache.length; }; -/** process the entry before caching */ -LoggingCache.prototype._preprocessEntry = function _preprocessEntry( entry ){ - return JSON.stringify( entry ); +/** get the entries from localStorage and parse them */ +LoggingCache.prototype._fetchAndParse = function _fetchAndParse(){ + var self = this; + return JSON.parse( localStorage.getItem( self.key ) ); }; +/** stringify the entries and put them in localStorage */ +LoggingCache.prototype._unparseAndStore = function _unparseAndStore( entries ){ + var self = this; + return localStorage.setItem( self.key, JSON.stringify( entries ) ); +}; + +///** process the entry before caching */ +//LoggingCache.prototype._preprocessEntry = function _preprocessEntry( entry ){ +// return JSON.stringify( entry ); +//}; + /** return the length --- oh, getters where are you? */ LoggingCache.prototype.length = function length(){ - return this._cache.length; + return this._fetchAndParse().length; }; /** get count number of entries starting with the oldest */ LoggingCache.prototype.get = function get( count ){ - return this._cache.slice( 0, count ); + return this._fetchAndParse().slice( 0, count ); }; /** remove count number of entries starting with the oldest */ LoggingCache.prototype.remove = function remove( count ){ - return this._cache.splice( 0, count ); + var _cache = this._fetchAndParse(), + removed = _cache.splice( 0, count ); + this._unparseAndStore( _cache ); + return removed; +}; + +/** empty/clear the entire cache */ +LoggingCache.prototype.empty = function empty(){ + localStorage.setItem( this.key, '[]' ); + return this; }; /** stringify count number of entries (but do not remove) */ LoggingCache.prototype.stringify = function stringify( count ){ - return [ '[', ( this.get( count ).join( ',\n' ) ), ']' ].join( '\n' ); + return JSON.stringify( this.get( count ) ); }; /** outputs entire cache to console */ LoggingCache.prototype.print = function print(){ // popup? (really, carl? a popup?) - easier to copy/paste - this._cache.forEach( function( entry ){ - console.log( entry ); - }); + console.log( JSON.stringify( this._fetchAndParse(), null, ' ' ) ); }; diff -r f1c9df0b5cabb998dd82dc6a154f66677173ffb8 -r 6f8070735e6f36fd1cd7b79d7cef1d69ebf8c1d6 test/qunit/tests/metrics-logger.js --- a/test/qunit/tests/metrics-logger.js +++ b/test/qunit/tests/metrics-logger.js @@ -22,9 +22,51 @@ self.lastMessage = { level: fnName, args: args }; }; }); + return self; + }; + + var MockLocalStorage = function(){ + var self = this; + self._storage = {}; + self.setItem = function( k, v ){ + self._storage[ k ] = v + ''; + return undefined; + }; + self.getItem = function( k ){ + return self._storage.hasOwnProperty( k )? self._storage[ k ]: null; + }; + self.removeItem = function( k ){ + if( self._storage.hasOwnProperty( k ) ){ + delete self._storage[ k ]; + } + return undefined; + }; + self.key = function( i ){ + var index = 0; + for( var k in self._storage ){ + if( self._storage.hasOwnProperty( k ) ){ + if( i === index ){ return k; } + index += 1; + } + } + return null; + }; + self.clear = function(){ + self._storage = {}; + }; + // property - not worth it + //self.length = function(){}; + + return self; + }; + window.localStorage = new MockLocalStorage(); + + ( window.bootstrapped = {} ).user = { + id : 'test' }; module( "Metrics logger tests" ); + console.debug( '\n' ); // ======================================================================== MetricsLogger test( "logger construction/initializiation defaults", function() { var logger = new metrics.MetricsLogger({}); @@ -68,11 +110,14 @@ var logger = new metrics.MetricsLogger({ logLevel : 'metric' }); + logger.cache.empty(); + equal( logger.options.logLevel, metrics.MetricsLogger.METRIC ); logger.emit( 'metric', 'test', [ 1, 2, { three: 3 }] ); equal( logger.cache.length(), 1 ); - var cached = JSON.parse( logger.cache.get( 1 ) ); + var cached = logger.cache.get( 1 )[0]; + //console.debug( 'cached:', JSON.stringify( cached ) ); equal( cached.level, metrics.MetricsLogger.METRIC ); equal( cached.namespace, 'client.test' ); equal( cached.args.length, 3 ); @@ -85,6 +130,8 @@ var logger = new metrics.MetricsLogger({ logLevel : 'metric' }); + logger.cache.empty(); + logger.emit( 'error', 'test', [ 1, 2, { three: 3 }] ); equal( logger.cache.length(), 0 ); }); @@ -93,6 +140,8 @@ var logger = new metrics.MetricsLogger({ logLevel : 'metric' }); + logger.cache.empty(); + logger.emit( 'metric', 'test', [{ window: window }] ); equal( logger.cache.length(), 0 ); }); @@ -108,11 +157,14 @@ logLevel : 'metric', onServerResponse : function( response ){ callback(); } }); + logger.cache.empty(); var server = sinon.fakeServer.create(), metricsOnServer; server.respondWith( 'POST', '/api/metrics', function( request ){ metricsOnServer = metricsFromRequestBody( request ); + //console.debug( 'requestBody:', request.requestBody ); + //console.debug( 'metricsOnServer:', JSON.stringify( metricsOnServer, null, ' ' ) ); request.respond( 200, { "Content-Type": "application/json" }, @@ -125,6 +177,7 @@ logger.emit( 'metric', 'test', [ 1, 2, { three: 3 }] ); logger._postCache(); server.respond(); + ok( callback.calledOnce, 'onServerResponse was called' ); equal( logger.cache.length(), 0, 'should have emptied cache (on success)' ); equal( logger._postSize, 1000, '_postSize still at default' ); @@ -148,6 +201,7 @@ logLevel : 'metric', onServerResponse : function( response ){ callback(); } }); + logger.cache.empty(); var server = sinon.fakeServer.create(); server.respondWith( 'POST', '/api/metrics', function( request ){ @@ -168,8 +222,6 @@ equal( logger.cache.length(), 1, 'should NOT have emptied cache' ); equal( logger._postSize, logger.options.maxCacheSize, '_postSize changed to max' ); - //TODO: still doesn't solve the problem that when cache == max, post will be tried on every emit - server.restore(); }); @@ -207,6 +259,8 @@ var logger = new metrics.MetricsLogger({ logLevel : 'all' }); + logger.cache.empty(); + equal( logger.options.logLevel, metrics.MetricsLogger.ALL ); logger.log( 0 ); logger.debug( 1 ); @@ -216,7 +270,7 @@ logger.metric( 5 ); equal( logger.cache.length(), 6 ); - var cached = logger.cache.remove( 6 ).map( JSON.parse ), + var cached = logger.cache.remove( 6 ), entry; cached.forEach( function( entry ){ @@ -240,22 +294,39 @@ // ======================================================================== LoggingCache test( "cache construction/initializiation defaults", function() { - var cache = new metrics.LoggingCache(); + // use empty to prevent tests stepping on one another due to persistence + var cache = new metrics.LoggingCache({ key: 'logs-test' }).empty(); equal( cache.maxSize, 5000 ); - equal( $.type( cache._cache ), 'array' ); + equal( window.localStorage.getItem( 'logs-test' ), '[]' ); + }); + + test( "cache construction/initializiation failure", function() { + ////TODO: doesn't work - readonly + //window.localStorage = null; + //console.debug( 'localStorage:', window.localStorage ); + var oldFn = metrics.LoggingCache.prototype._hasStorage; + metrics.LoggingCache.prototype._hasStorage = function(){ return false; }; + throws( function(){ + return new metrics.LoggingCache({ key: 'logs-test' }); + }, /LoggingCache needs localStorage/, 'lack of localStorage throws error' ); + metrics.LoggingCache.prototype._hasStorage = oldFn; + + throws( function(){ + return new metrics.LoggingCache(); + }, /LoggingCache needs key for localStorage/, 'lack of key throws error' ); }); test( "cache construction/initializiation setting max cache size", function() { var cache = new metrics.LoggingCache({ + key : 'logs-test', maxSize : 5 - }); + }).empty(); equal( cache.maxSize, 5 ); }); test( "cache plays well with no data", function() { - var cache = new metrics.LoggingCache(); + var cache = new metrics.LoggingCache({ key: 'logs-test' }).empty(); - equal( cache._cache.length, 0 ); equal( cache.length(), 0 ); var get = cache.get( 10 ); ok( jQuery.type( get ) === 'array' && get.length === 0 ); @@ -266,42 +337,59 @@ test( "cache add properly adds and removes data", function() { var cache = new metrics.LoggingCache({ + key : 'logs-test', maxSize : 5 - }); + }).empty(); + var entry1 = [{ one: 1 }, 'two' ]; cache.add( entry1 ); - equal( cache._cache.length, 1 ); equal( cache.length(), 1 ); - equal( cache._cache[0], JSON.stringify( entry1 ) ); - equal( cache.get( 1 ), JSON.stringify( entry1 ) ); + equal( JSON.stringify( cache.get( 1 )[0] ), JSON.stringify( entry1 ) ); var entry2 = { blah: { one: 1 }, bler: [ 'three', { two: 2 } ] }; cache.add( entry2 ); equal( cache.length(), 2 ); - equal( cache.stringify( 2 ), '[\n' + JSON.stringify( entry1 ) + ',\n' + JSON.stringify( entry2 ) + '\n]' ); + equal( cache.stringify( 2 ), '[' + JSON.stringify( entry1 ) + ',' + JSON.stringify( entry2 ) + ']' ); // FIFO var returned = cache.remove( 1 ); equal( cache.length(), 1 ); ok( jQuery.type( returned ) === 'array' && returned.length === 1 ); var returned0 = returned[0]; - ok( jQuery.type( returned0 ) === 'string' && returned0 === JSON.stringify( entry1 ) ); + ok( jQuery.type( returned0 ) === 'array' && JSON.stringify( returned0 ) === JSON.stringify( entry1 ) ); }); test( "cache past max loses oldest", function() { var cache = new metrics.LoggingCache({ + key : 'logs-test', maxSize : 5 - }); + }).empty(); + for( var i=0; i<10; i+=1 ){ cache.add({ index: i }); } equal( cache.length(), 5 ); var get = cache.get( 5 ); - ok( JSON.parse( get[0] ).index === 5 ); - ok( JSON.parse( get[1] ).index === 6 ); - ok( JSON.parse( get[2] ).index === 7 ); - ok( JSON.parse( get[3] ).index === 8 ); - ok( JSON.parse( get[4] ).index === 9 ); + ok( get[0].index === 5 ); + ok( get[1].index === 6 ); + ok( get[2].index === 7 ); + ok( get[3].index === 8 ); + ok( get[4].index === 9 ); }); + + test( "cache is properly persistent", function() { + var cache1 = new metrics.LoggingCache({ key : 'logs-test' }).empty(), + entry = [{ one: 1 }, 'two' ]; + cache1.add( entry ); + equal( cache1.length(), 1 ); + + var cache2 = new metrics.LoggingCache({ key : 'logs-test' }); + equal( cache2.length(), 1, 'old key gets previously stored' ); + equal( JSON.stringify( cache2.get( 1 )[0] ), JSON.stringify( entry ) ); + + var cache3 = new metrics.LoggingCache({ key : 'logs-bler' }); + equal( cache3.length(), 0, 'new key causes new storage' ); + }); + }); Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.