diff --git a/assets/tracer.js b/assets/tracer.js index 874c442..ecd1545 100644 --- a/assets/tracer.js +++ b/assets/tracer.js @@ -95,13 +95,14 @@ extendZone({ // but actions may happen even after the zone completed // and we are not interested about those if(this._events) { + event.time = this.getTime(); this._events.push(event); } }, setInfo: function(key, value) { - console.log(key, value); if(this._info) { + value.time = this.getTime(); this._info[key] = value; } }, @@ -115,6 +116,7 @@ extendZone({ }, setOwner: function(ownerInfo) { + ownerInfo.time = this.getTime(); this.owner = ownerInfo; }, @@ -130,7 +132,6 @@ extendZone({ if(ownerInfo) { zone.setOwner(ownerInfo); ownerInfo.zoneId = zone.id; - this.addEvent(ownerInfo); } return function zoneBoundFn() { @@ -148,6 +149,10 @@ extendZone({ boundZone.dequeueTask(func); return result; }, false, ownerInfo, validateArgs); + }, + + getTime: function () { + return Date.now(); } }); diff --git a/assets/utils.js b/assets/utils.js index e4f467c..2ee7ea3 100644 --- a/assets/utils.js +++ b/assets/utils.js @@ -9,12 +9,16 @@ function hijackConnection(original, type) { if(!isFromCall && args.length) { var callback = args[args.length - 1]; if(typeof callback === 'function') { - var ownerInfo = { - type: type, - name: args[0], - args: args.slice(1, args.length - 1) - }; - args[args.length - 1] = zone.bind(callback, false, ownerInfo, pickAllArgs); + var methodName = args[0]; + var methodArgs = args.slice(1, args.length - 1); + var ownerInfo = {type: type, name: methodName, args: methodArgs}; + args[args.length - 1] = function (argument) { + var args = Array.prototype.slice.call(arguments); + var zoneInfo = {type: type, name: methodName, args: methodArgs}; + zone.setInfo(type, zoneInfo); + return callback.apply(this, args); + } + args[args.length - 1] = zone.bind(args[args.length - 1], false, ownerInfo, pickAllArgs); } } @@ -34,22 +38,27 @@ function hijackSubscribe(originalFunction, type) { var args = Array.prototype.slice.call(arguments); if(args.length) { var callback = args[args.length - 1]; + var subName = args[0]; + var subArgs = args.slice(1, args.length - 1); if(typeof callback === 'function') { - var ownerInfo = { - type: type, - name: args[0], - args: args.slice(1, args.length - 1) - }; - args[args.length - 1] = zone.bind(callback, false, ownerInfo, pickAllArgs); + var ownerInfo = {type: type, name: subName, args: subArgs}; + args[args.length - 1] = function (argument) { + var args = Array.prototype.slice.call(arguments); + var zoneInfo = {type: type, name: subName, args: subArgs}; + zone.setInfo(type, zoneInfo); + return callback.apply(this, args); + } + args[args.length - 1] = zone.bind(args[args.length - 1], false, ownerInfo, pickAllArgs); } else if(callback) { ['onReady', 'onError'].forEach(function (funName) { - var ownerInfo = { - type: type, - name: args[0], - args: args.slice(1, args.length - 1), - callbackType: funName - }; + var ownerInfo = {type: type, name: subName, args: subArgs, callbackType: funName}; if(typeof callback[funName] === "function") { + callback[funName] = function (argument) { + var args = Array.prototype.slice.call(arguments); + var zoneInfo = {type: type, name: subName, args: subArgs, callbackType: funName}; + zone.setInfo(type, zoneInfo); + return callback.apply(this, args); + } callback[funName] = zone.bind(callback[funName], false, ownerInfo, pickAllArgs); } }) @@ -71,6 +80,38 @@ function hijackCursor(Cursor) { 'removed', 'movedBefore' ]); + ['fetch', 'forEach', 'map'].forEach(function (name) { + var original = Cursor[name]; + Cursor[name] = function (callback, thisArg) { + var self = thisArg || this; + var args = Array.prototype.slice.call(arguments); + var type = 'MongoCursor.' + name; + var notFromForEach = Zone.notFromForEach.get(); + if(!this._avoidZones + && !notFromForEach + && typeof callback === 'function') { + args[0] = function (doc, index) { + var args = Array.prototype.slice.call(arguments); + var ownerInfo = {type: type, collection: self.collection.name}; + var zoneInfo = {type: type, collection: self.collection.name}; + zoneInfo.document = doc; + zoneInfo.index = index; + zone.setInfo(type, zoneInfo); + callback = zone.bind(callback, false, ownerInfo. pickAllArgs); + return callback.apply(this, args); + }; + } + + if(name !== 'forEach') { + return Zone.notFromForEach.withValue(true, function() { + return original.apply(self, args); + }); + } else { + return original.apply(self, args); + } + } + }); + function hijackFunction(type, callbacks) { var original = Cursor[type]; Cursor[type] = function (options) { @@ -79,15 +120,25 @@ function hijackCursor(Cursor) { // if so, we don't need to track this request var isFromObserve = Zone.fromObserve.get(); - if(!isFromObserve && options) { + if(!this._avoidZones && !isFromObserve && options) { callbacks.forEach(function (funName) { - var ownerInfo = { - type: 'MongoCursor.' + type, - callbackType: funName, - collection: self.collection.name - }; - - if(typeof options[funName] === 'function') { + var callback = options[funName]; + if(typeof callback === 'function') { + var ownerInfo = { + type: 'MongoCursor.' + type, + callbackType: funName, + collection: self.collection.name + }; + options[funName] = function () { + var args = Array.prototype.slice.call(arguments); + var zoneInfo = { + type: 'MongoCursor.' + type, + callbackType: funName, + collection: self.collection.name + }; + zone.setInfo(type, zoneInfo); + return callback.apply(this, args); + } options[funName] = zone.bind(options[funName], false, ownerInfo, pickAllArgs); } }); @@ -106,6 +157,7 @@ function hijackCursor(Cursor) { } function hijackComponentEvents(original) { + var type = 'Template.event'; return function (dict) { var self = this; var name = this.__templateName || this.kind.split('_')[1]; @@ -118,11 +170,10 @@ function hijackComponentEvents(original) { function prepareHandler(handler, target) { return function () { var args = Array.prototype.slice.call(arguments); - zone.owner = { - type: 'Template.event', - event: target, - template: name - }; + var ownerInfo = {type: type, event: target, template: name}; + zone.owner = ownerInfo; + var zoneInfo = {type: type, event: target, template: name}; + zone.setInfo(type, zoneInfo); handler.apply(this, args); }; } @@ -130,14 +181,6 @@ function hijackComponentEvents(original) { } } -function hijackTemplateRendered(original, name) { - return function () { - var args = Array.prototype.slice.call(arguments); - zone.addEvent({type: 'Template.rendered', template: name}); - return original.apply(this, args); - } -} - function hijackDepsFlush(original, type) { return function () { var args = Array.prototype.slice.call(arguments); @@ -156,6 +199,70 @@ function hijackSessionSet(original, type) { } } +var TemplateCoreFunctions = ['prototype', '__makeView', '__render']; + +function hijackTemplateHelpers(template, templateName) { + _.each(template, function (hookFn, name) { + template[name] = hijackHelper(hookFn, name, templateName); + }); +} + +function hijackNewTemplateHelpers(original, templateName) { + return function (dict) { + dict && _.each(dict, function (hookFn, name) { + dict[name] = hijackHelper(hookFn, name, templateName); + }); + + var args = Array.prototype.slice.call(arguments); + return original.apply(this, args); + } +} + +function hijackHelper(hookFn, name, templateName) { + if(hookFn + && typeof hookFn === 'function' + && _.indexOf(TemplateCoreFunctions, name) === -1) { + // Assuming the value is a template helper + return function () { + var args = Array.prototype.slice.call(arguments); + zone.setInfo('Template.helper', {name: name, template: templateName}); + var result = hookFn.apply(this, args); + if(result && typeof result.observe === 'function') { + result._avoidZones = true; + } + return result; + } + } else { + return hookFn; + } +} + +function hijackGlobalHelpers(helpers) { + var _ = Package.underscore._; + _(helpers || {}).each(function (helperFn, name) { + helpers[name] = hijackGlobalHelper(helperFn, name) + }); +} + +function hijackGlobalHelper(helperFn, name) { + var _ = Package.underscore._; + if(helperFn + && typeof helperFn === 'function' + && _.indexOf(TemplateCoreFunctions, name) === -1) { + return function () { + var args = Array.prototype.slice.call(arguments); + zone.setInfo('Global.helper', {name: name}); + var result = helperFn.apply(this, args); + if(result && typeof result.observe === 'function') { + result._avoidZones = true; + } + return result; + } + } else { + return helperFn; + } +} + //--------------------------------------------------------------------------\\ var routerEvents = [ @@ -178,7 +285,12 @@ function hijackRouterConfigure(original, type) { name: this.route.name, path: this.path }); - hookFn.apply(this, args); + zone.setInfo('irHook', { + name: this.route.name, + hook: hookName, + path: this.path + }); + return hookFn.apply(this, args); } } }); @@ -189,6 +301,10 @@ function hijackRouterConfigure(original, type) { function hijackRouterGlobalHooks(Router, type) { routerEvents.forEach(function (hookName) { var hookFn = Router[hookName]; + /** + * Example + * Router.onBeforeAction( handler-function, options ) + */ Router[hookName] = function (hook, options) { var args = Array.prototype.slice.call(arguments); var hook = args[0]; @@ -202,10 +318,15 @@ function hijackRouterGlobalHooks(Router, type) { name: this.route.name, path: this.path }); - hook.apply(this, args); + zone.setInfo('irHook', { + name: this.route.name, + hook: hookName, + path: this.path + }); + return hook.apply(this, args); } } - hookFn.apply(this, args); + return hookFn.apply(this, args); } }); @@ -228,12 +349,17 @@ function hijackRouterOptions(original, type) { name: this.route.name, path: this.path }); - hookFn.apply(this, args); + zone.setInfo('irHook', { + name: this.route.name, + hook: hookName, + path: this.path + }); + return hookFn.apply(this, args); } } }); - original.apply(this, args); + return original.apply(this, args); } } @@ -251,11 +377,15 @@ function hijackRouteController(original, type) { name: this.route.name, path: this.path }); - hookFn.apply(this, args); + zone.setInfo('irHook', { + name: this.route.name, + hook: hookName, + path: this.path + }); + return hookFn.apply(this, args); } } }); - zone.addEvent({type: type}); return original.apply(this, args); } } diff --git a/client/hijack.js b/client/hijack.js index 03d2002..5255454 100644 --- a/client/hijack.js +++ b/client/hijack.js @@ -2,32 +2,37 @@ // see /assests/utils.js Zone.fromCall = new Meteor.EnvironmentVariable(); Zone.fromObserve = new Meteor.EnvironmentVariable(); +Zone.notFromForEach = new Meteor.EnvironmentVariable(); var ConnectionProto = getConnectionProto(); /* * Hijack method calls */ -var original_Connection_apply = ConnectionProto.apply; -ConnectionProto.apply = hijackConnection(original_Connection_apply, 'Connection.apply'); +ConnectionProto.apply = hijackConnection( + ConnectionProto.apply, + 'Connection.apply' +); -// for better stackTraces -var originalMeteorCall = Meteor.call; -Meteor.call = hijackConnection(originalMeteorCall, 'Meteor.call'); +/** + * For better stackTraces + */ +Meteor.call = hijackConnection(Meteor.call, 'Meteor.call'); /* * Hijack DDP subscribe method * Used when connecting to external DDP servers */ -var original_Connection_subscribe = ConnectionProto.subscribe; -ConnectionProto.subscribe = hijackSubscribe(original_Connection_subscribe, 'Connection.subscribe'); +ConnectionProto.subscribe = hijackSubscribe( + ConnectionProto.subscribe, + 'Connection.subscribe' +); /** * Hijack Meteor.subscribe because Meteor.subscribe binds to * Connection.subscribe before the hijack */ -var original_Meteor_subscribe = Meteor.subscribe; -Meteor.subscribe = hijackSubscribe(original_Meteor_subscribe, 'Meteor.subscribe'); +Meteor.subscribe = hijackSubscribe(Meteor.subscribe, 'Meteor.subscribe'); hijackCursor(LocalCollection.Cursor.prototype); @@ -42,27 +47,37 @@ if(Template.prototype) { UI.Component.events = hijackComponentEvents(UI.Component.events); } +/** + * Hijack global template helpers using `UI.registerHelper` + */ +hijackGlobalHelpers(UI._globalHelpers); + /** * Hijack each templates rendered handler to add template name to owner info */ +var CoreTemplates = ['prototype', '__body__', '__dynamic', '__dynamicWithDataContext', '__IronDefaultLayout__']; Meteor.startup(function () { _(Template).each(function (template, name) { - if(typeof template === 'object' && typeof template.rendered == 'function') { - var original = template.rendered; - template.rendered = hijackTemplateRendered(original, name); + if(typeof template === 'object') { + // hijack template helpers including 'rendered' + if(_.indexOf(CoreTemplates, name) === -1) { + hijackTemplateHelpers(template, name); + template.helpers = hijackNewTemplateHelpers(template.helpers, name); + } } }); }); -var originalSessionSet = Session.set; -Session.set = hijackSessionSet(originalSessionSet, 'Session.set'); +/** + * Hijack Session.set to add events + */ +Session.set = hijackSessionSet(Session.set, 'Session.set'); /** * Hijack Deps.autorun to set correct zone owner type * Otherwise these will be setTimeout */ -var originalDepsFlush = Deps.flush; -Deps.flush = hijackDepsFlush(originalDepsFlush, 'Deps.flush'); +Deps.flush = hijackDepsFlush(Deps.flush, 'Deps.flush'); /** * Hijack IronRouter if it's available diff --git a/package.js b/package.js index 3c0f37b..cb5170c 100644 --- a/package.js +++ b/package.js @@ -55,6 +55,7 @@ function addPackageFiles(api) { 'client/hijack.js' ], 'client'); + api.use('underscore', 'client'); api.use('ui', 'client'); api.use('templating', 'client'); api.use('deps', 'client'); diff --git a/tests/hijacks/collections.js b/tests/hijacks/collections.js index 966e26c..5a7812b 100644 --- a/tests/hijacks/collections.js +++ b/tests/hijacks/collections.js @@ -17,10 +17,32 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Connection.apply': { + type: 'Connection.apply', + name: '/test-collection/insert', + // time: 123, + args: [ + [{_id: 'foo', bar: 'baz'}], + {returnStubValue: true} + ], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Connection.apply'].time); + delete info['Connection.apply'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -54,10 +76,32 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Connection.apply': { + type: 'Connection.apply', + name: '/test-collection/update', + // time: 123, + args: [ + [{_id: 'foo'}, {$set: {bar: 'bat'}}, {}], + {returnStubValue: true} + ], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Connection.apply'].time); + delete info['Connection.apply'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -101,6 +145,8 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; if(owner.args[0][2].insertedId) { @@ -109,6 +155,39 @@ Tinytest.addAsync( delete owner.args[0][2].insertedId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Connection.apply': { + type: 'Connection.apply', + name: '/test-collection/update', + // time: 123, + args: [ + [ + {_id: 'foo'}, + {$set: {bar: 'bat'}}, + { + _returnObject: true, + upsert: true, + // insertedId: 'asd' + } + ], + {returnStubValue: true} + ], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Connection.apply'].time); + delete info['Connection.apply'].time; + var insertedId = info['Connection.apply'].args[0][2].insertedId; + if(insertedId) { + test.equal('string', typeof insertedId); + delete info['Connection.apply'].args[0][2].insertedId; + } + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -150,6 +229,8 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; if(owner.args[0][2].insertedId) { @@ -158,6 +239,39 @@ Tinytest.addAsync( delete owner.args[0][2].insertedId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Connection.apply': { + type: 'Connection.apply', + name: '/test-collection/update', + // time: 123, + args: [ + [ + {_id: 'foo'}, + {$set: {bar: 'bat'}}, + { + _returnObject: true, + upsert: true, + // insertedId: 'asd' + } + ], + {returnStubValue: true} + ], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Connection.apply'].time); + delete info['Connection.apply'].time; + var insertedId = info['Connection.apply'].args[0][2].insertedId; + if(insertedId) { + test.equal('string', typeof insertedId); + delete info['Connection.apply'].args[0][2].insertedId; + } + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -193,10 +307,32 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Connection.apply': { + type: 'Connection.apply', + name: '/test-collection/remove', + // time: 123, + args: [ + [{_id: 'foo'}], + {returnStubValue: true} + ], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Connection.apply'].time); + delete info['Connection.apply'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); diff --git a/tests/hijacks/methods.js b/tests/hijacks/methods.js index b833d93..6f38f7e 100644 --- a/tests/hijacks/methods.js +++ b/tests/hijacks/methods.js @@ -1,23 +1,43 @@ Tinytest.addAsync( - 'Hijacks - Methods - Meteor.call', + 'Hijacks - Methods - default', function (test, next) { Zone.Reporters.removeAll(); Zone.Reporters.add('test-reporter', function (zone) { // test whether zone has correct owner info var owner = zone.owner; - var expected = { + var expectedOwner = { args: ['arg1', 'arg2'], name: 'test', + // time: 123, type: 'Meteor.call', // zoneId: 123 }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; - test.equal(expected, owner); + test.equal(expectedOwner, owner); + + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Meteor.call': { + type: 'Meteor.call', + name: 'test', + // time: 123, + args: ['arg1', 'arg2'], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Meteor.call'].time); + delete info['Meteor.call'].time; + test.equal(expectedInfo, info); // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); diff --git a/tests/hijacks/subscriptions.js b/tests/hijacks/subscriptions.js index 7fd0c45..231c780 100644 --- a/tests/hijacks/subscriptions.js +++ b/tests/hijacks/subscriptions.js @@ -15,10 +15,29 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Meteor.subscribe': { + type: 'Meteor.subscribe', + name: 'test-ready', + // time: 123, + args: ['arg1', 'arg2'], + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Meteor.subscribe'].time); + delete info['Meteor.subscribe'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -48,10 +67,30 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Meteor.subscribe': { + type: 'Meteor.subscribe', + name: 'test-ready', + // time: 123, + args: ['arg1', 'arg2'], + callbackType: 'onReady' + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Meteor.subscribe'].time); + delete info['Meteor.subscribe'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter'); @@ -83,10 +122,30 @@ Tinytest.addAsync( }; test.equal('object', typeof owner); + test.equal('number', typeof owner.time); + delete owner.time; test.equal('number', typeof owner.zoneId); delete owner.zoneId; test.equal(expected, owner); + // test whether zone has correct info + // the parent zone contains method info + var info = zone.infoMap[zone.id]; + var expectedInfo = { + 'Meteor.subscribe': { + type: 'Meteor.subscribe', + name: 'test-error', + // time: 123, + args: ['arg1', 'arg2'], + callbackType: 'onError' + } + }; + + test.equal('object', typeof info); + test.equal('number', typeof info['Meteor.subscribe'].time); + delete info['Meteor.subscribe'].time; + test.equal(expectedInfo, info); + // reset zone for other tests and continue Zone.Reporters.add(Zone.longStackTrace); Zone.Reporters.remove('test-reporter');