├── .bowerrc ├── .gitignore ├── test ├── scripts │ └── setup.js └── index.html ├── package.json ├── bower.json ├── gulpfile.js ├── LICENSE ├── README.md ├── src └── ember-performance-sender.ts └── dist ├── ember-performance-sender.js ├── ember-performance-sender.cjs.js └── ember-performance-sender.amd.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "vendor" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | node_modules/ 4 | vendor/ 5 | tsconfig.json 6 | -------------------------------------------------------------------------------- /test/scripts/setup.js: -------------------------------------------------------------------------------- 1 | var App = Ember.Application.create(); 2 | 3 | App.initializer({ 4 | name: 'performance', 5 | initialize: function (container, application) { 6 | EmPerfSender.initialize({ 7 | send: function (events, metrics) { 8 | console.log(arguments); 9 | } 10 | }); 11 | } 12 | }); 13 | 14 | App.Router.map(function () { 15 | this.route('test'); 16 | }); 17 | 18 | App.TestView = Ember.View.extend({ 19 | templateName: 'test' 20 | }); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-performance-sender", 3 | "version": "1.1.0", 4 | "description": "Client-side interface for Weppy performance sendering tool", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/Wikia/ember-performance-sender.git" 8 | }, 9 | "scripts": { 10 | "test": "node tests/qunit-runner.js" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "gulp": "3.x.x", 15 | "gulp-rename": "1.2.x", 16 | "gulp-typescript": "2.x.x", 17 | "gulp-wrap": "0.5.x", 18 | "qunit": "0.x.x" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-performance-sender", 3 | "main": "ember-performance-sender.js", 4 | "version": "1.2.1", 5 | "homepage": "https://github.com/Wikia/ember-performance-sender.git", 6 | "authors": [ 7 | "Igor Rogatty ", 8 | "Kenneth Kouot 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | rename = require('gulp-rename'), 3 | ts = require('gulp-typescript'), 4 | wrap = require('gulp-wrap'), 5 | source = './src/*.ts', 6 | destination = './dist'; 7 | 8 | gulp.task('compile', function () { 9 | return gulp.src(source) 10 | .pipe(ts()).js 11 | .pipe(gulp.dest(destination)); 12 | }); 13 | 14 | gulp.task('compileAMD', function () { 15 | return gulp.src(source) 16 | .pipe(wrap('export <%= contents %>')) 17 | .pipe(ts({module: 'amd'})).js 18 | .pipe(rename(function (path) { 19 | path.basename += '.amd'; 20 | })) 21 | .pipe(gulp.dest(destination)); 22 | }); 23 | 24 | gulp.task('compileCommonJS', function () { 25 | return gulp.src(source) 26 | .pipe(wrap('export <%= contents %>')) 27 | .pipe(ts({module: 'commonjs'})).js 28 | .pipe(rename(function (path) { 29 | path.basename += '.cjs'; 30 | })) 31 | .pipe(gulp.dest(destination)); 32 | }); 33 | 34 | gulp.task('default', ['compile', 'compileAMD', 'compileCommonJS']); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wikia, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Performance Sender 2 | This plugin extends Ember to allow for the collection of performance data during route, view and action events. This plugin is agnostic to where you choose to send your data as you can supply your own meaningful overrides for the plugin's various methods: `log()`, `warn()`, `error()`, and `send()`. 3 | 4 | ## Getting Started 5 | * To begin, you can install this plugin using Bower: `$ bower install Wikia/ember-performance-sender` 6 | * Next, include the appropriate [distribution](https://github.com/Wikia/ember-performance-sender/tree/master/dist) file in your dependencies, **after Ember.js, but before your Ember application code** 7 | * In your Ember application code, specify an initializer or initialize `EmPerfSender` in an existing initializer: 8 | ```js 9 | App.initializer({ 10 | name: 'performance', 11 | initialize: (container, application) => { 12 | EmPerfSender.initialize( /* options */); 13 | } 14 | }); 15 | ``` 16 | 17 | ## Initialization Options 18 | `EmPerfSender` can be initialized with the following options: 19 | 20 | #### `enableAjaxFilter: boolean` (default: false) 21 | If enabled, this will not call send for AJAX traces 22 | 23 | #### `enableLogging: boolean` (default: true) 24 | Will turn on verbose logging. 25 | 26 | #### `minDuration: number` (default: 50) 27 | Minimum time to call `send()` on 28 | 29 | #### `function send(events, metrics): void` (optional) 30 | #### `function error(message): void` (optional) 31 | #### `function log(message): void` (optional) 32 | #### `function warn(message) void` (optional) 33 | 34 | ## Attribution 35 | The was originally based on a fork of [Caliper's Ember adapter](https://github.com/caliper-io/caliper-ember-dist), but has since been heavily modified. Original library by [Godfrey Chan](https://twitter.com/chancancode). 36 | -------------------------------------------------------------------------------- /src/ember-performance-sender.ts: -------------------------------------------------------------------------------- 1 | module EmPerfSender { 2 | export interface MetricsReport { 3 | startTime: number; 4 | duration: number; 5 | url: string; 6 | klass?: string; 7 | method?: string; 8 | pattern?: string; 9 | } 10 | 11 | export interface EmPerfSenderConfig { 12 | enableAjaxFilter: boolean; 13 | enableLogging: boolean; 14 | error?: (message: string) => void; 15 | log?: (message: string) => void; 16 | minDuration: number; 17 | send: (events: any[], metrics: MetricsReport) => void; 18 | warn?: (message: string) => void; 19 | subclassPattern: RegExp; 20 | } 21 | 22 | export var VERSION: string = '1.1.1'; 23 | export var loaded: boolean = false; 24 | export var enabled: boolean = false; 25 | export var config: EmPerfSenderConfig = { 26 | enableAjaxFilter: false, 27 | enableLogging: true, 28 | minDuration: 50, 29 | log: (message) => { console.log(message); }, 30 | send: (events, metrics) => { console.log('Sending (Dry Run)\n============================\n', metrics); }, 31 | warn: (message) => { console.log('warn: ' + message); }, 32 | error: (message) => { console.log('error: ' + message); }, 33 | subclassPattern: new RegExp('<(?:\\(subclass of )?(.*?)\\)?:.*>') 34 | }; 35 | 36 | export var traceStack = []; 37 | 38 | function getLastTraceItem () { 39 | return traceStack[traceStack.length - 1]; 40 | } 41 | 42 | function getRoutePattern (EmberRoute) { 43 | try { 44 | var routeName = EmberRoute.get('routeName'), 45 | segments = EmberRoute.get('router.router.recognizer.names')[routeName].segments, 46 | listOfSegments = []; 47 | 48 | for (var i = 0; i < segments.length; i++) { 49 | var segment = null; 50 | try { 51 | segment = segments[i].generate(); 52 | } catch (err) { 53 | segment = ':' + segments[i].name; 54 | } 55 | 56 | if (segment) { 57 | listOfSegments.push(segment); 58 | } 59 | } 60 | return '/' + listOfSegments.join('/'); 61 | } catch (err) { 62 | return '/'; 63 | } 64 | } 65 | 66 | function traceRouteEvent () { 67 | var pattern = getRoutePattern(this), 68 | lastTrace = getLastTraceItem(); 69 | 70 | if (lastTrace) { 71 | lastTrace.klass = this.constructor.toString(); 72 | lastTrace.method = 'routeTransition'; 73 | lastTrace.pattern = pattern; 74 | } else { 75 | new Trace(this.constructor.toString(), 'routeTransition', pattern); 76 | } 77 | 78 | return this._super.apply(this, arguments); 79 | } 80 | 81 | function ifLoggingEnabled (method, ...args) { 82 | if (config.enableLogging && typeof config[method] === 'function') { 83 | return config[method].apply(this, args); 84 | } 85 | } 86 | 87 | function wrap (method) { 88 | var traceItem = getLastTraceItem(); 89 | 90 | if (traceItem) { 91 | if (typeof traceItem.pattern === undefined) { 92 | traceItem.klass = this.constructor.toString(); 93 | }; 94 | traceItem.method = method; 95 | } else { 96 | new Trace(this.constructor.toString(), method); 97 | } 98 | } 99 | 100 | function wrapEvent (eventName, callback) { 101 | if ('function' == typeof callback) { 102 | return decorate(callback, () => { 103 | wrap.call(this, eventName); 104 | }); 105 | } else { 106 | return callback; 107 | } 108 | } 109 | 110 | function decorate (orig, decorator) { 111 | return function () { 112 | var ret; 113 | try { 114 | ret = decorator.apply(this, arguments); 115 | } catch (e) { 116 | throw e; 117 | } 118 | return (typeof orig === 'function' ? orig : () => {}).apply(this, arguments); 119 | } 120 | } 121 | 122 | function aliasMethodChain ($, method, label, wrapper) { 123 | var originalMethod = $[method]; 124 | if ('function' == typeof originalMethod) { 125 | var methodWithout = '__' + method + '_without_' + label + '__', 126 | methodWith = '__' + method + '_with_' + label + '__'; 127 | 128 | $[methodWithout] = originalMethod; 129 | $[methodWith] = $[method] = wrapper; 130 | } 131 | return $; 132 | } 133 | 134 | function ajaxDecorator () { 135 | if (getLastTraceItem()) { 136 | var request = arguments[1] || arguments[0], 137 | type = request.type || 'GET', 138 | url = request.url || arguments[0]; 139 | 140 | if (typeof request === 'string') { 141 | request = {}; 142 | } 143 | var tracer = new AjaxTrace(type, url); 144 | 145 | request.success = decorate(request.success, () => { 146 | tracer.trace.resume(); 147 | tracer.stop(); 148 | }); 149 | 150 | request.error = decorate(request.error, () => { 151 | tracer.trace.resume(); 152 | tracer.stop(); 153 | }); 154 | 155 | request.url = url; 156 | } 157 | } 158 | 159 | class BaseTrace { 160 | pending: boolean; 161 | startTime: number; 162 | stopTime: number; 163 | trace: any; 164 | 165 | stop () { 166 | if (this.pending) { 167 | this.stopTime = Date.now(); 168 | this.pending = false; 169 | } else { 170 | ifLoggingEnabled('warn', '[BUG] ' + this.constructor['name'] + ': Attempted to stop a view render twice.') 171 | } 172 | } 173 | 174 | serialize (time: number = 0, ...additionalParams: string[]): any[] { 175 | if (this.pending === false) { 176 | var serialized = [this.constructor['name'], this.startTime - time, this.stopTime - this.startTime]; 177 | if (additionalParams.length) { 178 | serialized = serialized.concat(additionalParams); 179 | } 180 | return serialized; 181 | } 182 | } 183 | } 184 | 185 | class AjaxTrace extends BaseTrace { 186 | method: string; 187 | url: string; 188 | 189 | constructor (method, url) { 190 | this.method = method; 191 | this.url = url; 192 | this.pending = true; 193 | this.trace = getLastTraceItem(); 194 | if (this.trace) { 195 | this.trace.events.push(this); 196 | } 197 | this.startTime = Date.now(); 198 | 199 | super(); 200 | } 201 | 202 | serialize (time: number): any[] { 203 | return super.serialize(time, this.method, this.url); 204 | } 205 | } 206 | 207 | class ViewRenderTrace extends BaseTrace { 208 | viewName: string; 209 | 210 | constructor (viewName) { 211 | this.viewName = viewName; 212 | this.pending = true; 213 | this.trace = getLastTraceItem(); 214 | if (this.trace) { 215 | this.trace.events.push(this); 216 | } 217 | this.startTime = Date.now(); 218 | 219 | super(); 220 | } 221 | 222 | serialize (time: number): any[] { 223 | return super.serialize(time, this.viewName); 224 | } 225 | } 226 | 227 | class Trace extends BaseTrace { 228 | duration: number; 229 | events: Trace[]; 230 | finalized: boolean; 231 | klass: string; 232 | method: string; 233 | pattern: string; 234 | pending: boolean; 235 | startTime: number; 236 | stopTime: number; 237 | url: string; 238 | 239 | constructor (klass, method, pattern?) { 240 | if (klass.match('Window')) { 241 | throw TypeError(klass + ' is not a valid Trace class'); 242 | } 243 | this.klass = klass; 244 | this.method = method; 245 | this.pattern = pattern; 246 | this.events = []; 247 | this.finalized = false; 248 | this.startTime = Date.now(); 249 | traceStack.push(this); 250 | super(); 251 | } 252 | 253 | serialize (time: number): any[] { 254 | return super.serialize(time, this.klass, this.method, this.url); 255 | } 256 | 257 | pause () { 258 | for (var i = 1; i <= traceStack.length; i++) { 259 | if (traceStack[traceStack.length - i] === this) { 260 | traceStack.splice(traceStack.length - i, 1); 261 | } 262 | } 263 | } 264 | 265 | resume () { 266 | this.pause(); 267 | traceStack.push(this); 268 | } 269 | 270 | finalize () { 271 | if (this.finalized) { 272 | ifLoggingEnabled('warn', '[BUG] Attempted to finalize a trace twice.'); 273 | return; 274 | } 275 | 276 | this.pause(); 277 | 278 | for (var i = 0; i < this.events.length; i++) { 279 | if (this.events[i].pending) { 280 | return; 281 | } 282 | } 283 | 284 | this.stopTime = Date.now(); 285 | this.finalized = true; 286 | this.duration = this.stopTime - this.startTime; 287 | 288 | if (this.duration < (config.minDuration || 1)) { 289 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 290 | this.duration + 'ms. (minDuration is ' + config.minDuration + 'ms)'); 291 | return; 292 | } 293 | 294 | if (config.enableAjaxFilter === true) { 295 | var ajaxRequestToTrace = false; 296 | 297 | for (i = 0; i < this.events.length; i++) { 298 | if (this.events[i] instanceof AjaxTrace) { 299 | ajaxRequestToTrace = true; 300 | break; 301 | } 302 | } 303 | 304 | if (!ajaxRequestToTrace) { 305 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 306 | this.duration + 'ms. (enableAjaxFilter is true)'); 307 | return; 308 | } 309 | } 310 | 311 | ifLoggingEnabled('log', 'Sending: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + this.duration + 312 | 'ms.'); 313 | 314 | this.url = window.location.hash || window.location.pathname; 315 | 316 | var metrics: MetricsReport = { 317 | startTime: this.startTime, 318 | duration: this.duration, 319 | url: this.url 320 | }; 321 | 322 | if (this.klass && this.klass.length) { 323 | metrics.klass = this.klass; 324 | } 325 | if (this.method && this.method.length) { 326 | metrics.method = this.method; 327 | } 328 | if (this.pattern && this.pattern.length) { 329 | metrics.pattern = this.pattern; 330 | } 331 | 332 | var events = []; 333 | for (i = 0; i < this.events.length; i++) { 334 | events.push(this.events[i].serialize(this.startTime)); 335 | } 336 | 337 | config.send(events, metrics) 338 | } 339 | } 340 | 341 | export function initialize (userConfig: EmPerfSenderConfig, Ember = window['Ember'], $ = window['jQuery']) { 342 | 'use strict'; 343 | 344 | if (Ember === undefined) { 345 | throw ReferenceError('EmPerfSender cannot find Ember! Check that you have loaded Ember correctly before EmPerfSender!') 346 | } else if ($ === undefined) { 347 | throw ReferenceError('EmPerfSender cannot find jQuery! Make sure you have loaded jQuery before Ember and EmPerfSender!'); 348 | } else { 349 | config = $.extend(config, userConfig); 350 | ifLoggingEnabled('log','Initializing EmPerfSender v' + VERSION); 351 | 352 | Ember.Route.reopen({ 353 | beforeModel: traceRouteEvent, 354 | afterModel: traceRouteEvent, 355 | enter: traceRouteEvent 356 | // TODO this currently causes 'TypeError: Cannot set property '_qpDelegate' of undefined' 357 | //setup: traceRouteEvent 358 | }); 359 | 360 | Ember.CoreView.reopen({ 361 | /** 362 | * Triggers a named event for the object. Any additional arguments 363 | * will be passed as parameters to the functions that are subscribed to the 364 | * event. 365 | * 366 | * @method trigger 367 | * @param {String} eventName The name of the event 368 | * @param {Object...} args Optional arguments to pass on 369 | */ 370 | trigger (eventName) { 371 | var className = this.constructor.toString(), 372 | isNotEmberClass = ( 373 | 'Ember' !== className.substr(0, 5) && 374 | // (subclass of Ember...) 375 | 'Ember' !== className.substr(13, 5) 376 | ), 377 | isEvent = this.constructor.prototype.hasOwnProperty(eventName), 378 | shouldTrace = isNotEmberClass && isEvent; 379 | 380 | if (shouldTrace) { 381 | new Trace(this.constructor.toString(), eventName); 382 | } 383 | 384 | return this._super.apply(this, arguments); 385 | } 386 | }); 387 | 388 | Ember.ActionHandler.reopen({ 389 | willMergeMixin (props) { 390 | var eventName, 391 | nextSuper = this._super(props); 392 | 393 | if (props._actions) { 394 | for (eventName in props._actions) { 395 | if (props._actions.hasOwnProperty(eventName)) { 396 | props._actions[eventName] = wrapEvent.apply(this, [eventName, props._actions[eventName]]); 397 | } 398 | } 399 | } else if (props.events) { 400 | for (eventName in props.events) { 401 | if (props.events.hasOwnProperty(eventName)) { 402 | props.events[eventName] = wrapEvent.apply(this, [eventName, props.events[eventName]]); 403 | } 404 | } 405 | } 406 | return nextSuper; 407 | } 408 | }); 409 | 410 | Ember.subscribe('render.view', { 411 | before (eventName, time, container) { 412 | if (getLastTraceItem()) { 413 | var parentClass = container.object.match(config.subclassPattern)[1]; 414 | if ('Ember' !== parentClass.substr(0, 5) && 'LinkView' !== parentClass) { 415 | return new ViewRenderTrace(parentClass); 416 | } 417 | } 418 | }, 419 | after (eventName, time, container, tracer) { 420 | if (tracer) { 421 | tracer.stop(); 422 | } 423 | } 424 | }); 425 | 426 | Ember.run.backburner.options.onEnd = decorate(Ember.run.backburner.options.onEnd, (current, next) => { 427 | if (!next && traceStack.length) { 428 | for (var d = 0; d < traceStack.length; d++) { 429 | traceStack[d].finalize(); 430 | } 431 | } 432 | }); 433 | 434 | aliasMethodChain($, 'ajax', 'instrumentation', decorate($.ajax, ajaxDecorator)); 435 | loaded = true; 436 | } 437 | }; 438 | } 439 | -------------------------------------------------------------------------------- /dist/ember-performance-sender.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | __.prototype = b.prototype; 5 | d.prototype = new __(); 6 | }; 7 | var EmPerfSender; 8 | (function (EmPerfSender) { 9 | EmPerfSender.VERSION = '1.1.1'; 10 | EmPerfSender.loaded = false; 11 | EmPerfSender.enabled = false; 12 | EmPerfSender.config = { 13 | enableAjaxFilter: false, 14 | enableLogging: true, 15 | minDuration: 50, 16 | log: function (message) { console.log(message); }, 17 | send: function (events, metrics) { console.log('Sending (Dry Run)\n============================\n', metrics); }, 18 | warn: function (message) { console.log('warn: ' + message); }, 19 | error: function (message) { console.log('error: ' + message); }, 20 | subclassPattern: new RegExp('<(?:\\(subclass of )?(.*?)\\)?:.*>') 21 | }; 22 | EmPerfSender.traceStack = []; 23 | function getLastTraceItem() { 24 | return EmPerfSender.traceStack[EmPerfSender.traceStack.length - 1]; 25 | } 26 | function getRoutePattern(EmberRoute) { 27 | try { 28 | var routeName = EmberRoute.get('routeName'), segments = EmberRoute.get('router.router.recognizer.names')[routeName].segments, listOfSegments = []; 29 | for (var i = 0; i < segments.length; i++) { 30 | var segment = null; 31 | try { 32 | segment = segments[i].generate(); 33 | } 34 | catch (err) { 35 | segment = ':' + segments[i].name; 36 | } 37 | if (segment) { 38 | listOfSegments.push(segment); 39 | } 40 | } 41 | return '/' + listOfSegments.join('/'); 42 | } 43 | catch (err) { 44 | return '/'; 45 | } 46 | } 47 | function traceRouteEvent() { 48 | var pattern = getRoutePattern(this), lastTrace = getLastTraceItem(); 49 | if (lastTrace) { 50 | lastTrace.klass = this.constructor.toString(); 51 | lastTrace.method = 'routeTransition'; 52 | lastTrace.pattern = pattern; 53 | } 54 | else { 55 | new Trace(this.constructor.toString(), 'routeTransition', pattern); 56 | } 57 | return this._super.apply(this, arguments); 58 | } 59 | function ifLoggingEnabled(method) { 60 | var args = []; 61 | for (var _i = 1; _i < arguments.length; _i++) { 62 | args[_i - 1] = arguments[_i]; 63 | } 64 | if (EmPerfSender.config.enableLogging && typeof EmPerfSender.config[method] === 'function') { 65 | return EmPerfSender.config[method].apply(this, args); 66 | } 67 | } 68 | function wrap(method) { 69 | var traceItem = getLastTraceItem(); 70 | if (traceItem) { 71 | if (typeof traceItem.pattern === undefined) { 72 | traceItem.klass = this.constructor.toString(); 73 | } 74 | ; 75 | traceItem.method = method; 76 | } 77 | else { 78 | new Trace(this.constructor.toString(), method); 79 | } 80 | } 81 | function wrapEvent(eventName, callback) { 82 | var _this = this; 83 | if ('function' == typeof callback) { 84 | return decorate(callback, function () { 85 | wrap.call(_this, eventName); 86 | }); 87 | } 88 | else { 89 | return callback; 90 | } 91 | } 92 | function decorate(orig, decorator) { 93 | return function () { 94 | var ret; 95 | try { 96 | ret = decorator.apply(this, arguments); 97 | } 98 | catch (e) { 99 | throw e; 100 | } 101 | return (typeof orig === 'function' ? orig : function () { }).apply(this, arguments); 102 | }; 103 | } 104 | function aliasMethodChain($, method, label, wrapper) { 105 | var originalMethod = $[method]; 106 | if ('function' == typeof originalMethod) { 107 | var methodWithout = '__' + method + '_without_' + label + '__', methodWith = '__' + method + '_with_' + label + '__'; 108 | $[methodWithout] = originalMethod; 109 | $[methodWith] = $[method] = wrapper; 110 | } 111 | return $; 112 | } 113 | function ajaxDecorator() { 114 | if (getLastTraceItem()) { 115 | var request = arguments[1] || arguments[0], type = request.type || 'GET', url = request.url || arguments[0]; 116 | if (typeof request === 'string') { 117 | request = {}; 118 | } 119 | var tracer = new AjaxTrace(type, url); 120 | request.success = decorate(request.success, function () { 121 | tracer.trace.resume(); 122 | tracer.stop(); 123 | }); 124 | request.error = decorate(request.error, function () { 125 | tracer.trace.resume(); 126 | tracer.stop(); 127 | }); 128 | request.url = url; 129 | } 130 | } 131 | var BaseTrace = (function () { 132 | function BaseTrace() { 133 | } 134 | BaseTrace.prototype.stop = function () { 135 | if (this.pending) { 136 | this.stopTime = Date.now(); 137 | this.pending = false; 138 | } 139 | else { 140 | ifLoggingEnabled('warn', '[BUG] ' + this.constructor['name'] + ': Attempted to stop a view render twice.'); 141 | } 142 | }; 143 | BaseTrace.prototype.serialize = function (time) { 144 | if (time === void 0) { time = 0; } 145 | var additionalParams = []; 146 | for (var _i = 1; _i < arguments.length; _i++) { 147 | additionalParams[_i - 1] = arguments[_i]; 148 | } 149 | if (this.pending === false) { 150 | var serialized = [this.constructor['name'], this.startTime - time, this.stopTime - this.startTime]; 151 | if (additionalParams.length) { 152 | serialized = serialized.concat(additionalParams); 153 | } 154 | return serialized; 155 | } 156 | }; 157 | return BaseTrace; 158 | })(); 159 | var AjaxTrace = (function (_super) { 160 | __extends(AjaxTrace, _super); 161 | function AjaxTrace(method, url) { 162 | this.method = method; 163 | this.url = url; 164 | this.pending = true; 165 | this.trace = getLastTraceItem(); 166 | if (this.trace) { 167 | this.trace.events.push(this); 168 | } 169 | this.startTime = Date.now(); 170 | _super.call(this); 171 | } 172 | AjaxTrace.prototype.serialize = function (time) { 173 | return _super.prototype.serialize.call(this, time, this.method, this.url); 174 | }; 175 | return AjaxTrace; 176 | })(BaseTrace); 177 | var ViewRenderTrace = (function (_super) { 178 | __extends(ViewRenderTrace, _super); 179 | function ViewRenderTrace(viewName) { 180 | this.viewName = viewName; 181 | this.pending = true; 182 | this.trace = getLastTraceItem(); 183 | if (this.trace) { 184 | this.trace.events.push(this); 185 | } 186 | this.startTime = Date.now(); 187 | _super.call(this); 188 | } 189 | ViewRenderTrace.prototype.serialize = function (time) { 190 | return _super.prototype.serialize.call(this, time, this.viewName); 191 | }; 192 | return ViewRenderTrace; 193 | })(BaseTrace); 194 | var Trace = (function (_super) { 195 | __extends(Trace, _super); 196 | function Trace(klass, method, pattern) { 197 | if (klass.match('Window')) { 198 | throw TypeError(klass + ' is not a valid Trace class'); 199 | } 200 | this.klass = klass; 201 | this.method = method; 202 | this.pattern = pattern; 203 | this.events = []; 204 | this.finalized = false; 205 | this.startTime = Date.now(); 206 | EmPerfSender.traceStack.push(this); 207 | _super.call(this); 208 | } 209 | Trace.prototype.serialize = function (time) { 210 | return _super.prototype.serialize.call(this, time, this.klass, this.method, this.url); 211 | }; 212 | Trace.prototype.pause = function () { 213 | for (var i = 1; i <= EmPerfSender.traceStack.length; i++) { 214 | if (EmPerfSender.traceStack[EmPerfSender.traceStack.length - i] === this) { 215 | EmPerfSender.traceStack.splice(EmPerfSender.traceStack.length - i, 1); 216 | } 217 | } 218 | }; 219 | Trace.prototype.resume = function () { 220 | this.pause(); 221 | EmPerfSender.traceStack.push(this); 222 | }; 223 | Trace.prototype.finalize = function () { 224 | if (this.finalized) { 225 | ifLoggingEnabled('warn', '[BUG] Attempted to finalize a trace twice.'); 226 | return; 227 | } 228 | this.pause(); 229 | for (var i = 0; i < this.events.length; i++) { 230 | if (this.events[i].pending) { 231 | return; 232 | } 233 | } 234 | this.stopTime = Date.now(); 235 | this.finalized = true; 236 | this.duration = this.stopTime - this.startTime; 237 | if (this.duration < (EmPerfSender.config.minDuration || 1)) { 238 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 239 | this.duration + 'ms. (minDuration is ' + EmPerfSender.config.minDuration + 'ms)'); 240 | return; 241 | } 242 | if (EmPerfSender.config.enableAjaxFilter === true) { 243 | var ajaxRequestToTrace = false; 244 | for (i = 0; i < this.events.length; i++) { 245 | if (this.events[i] instanceof AjaxTrace) { 246 | ajaxRequestToTrace = true; 247 | break; 248 | } 249 | } 250 | if (!ajaxRequestToTrace) { 251 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 252 | this.duration + 'ms. (enableAjaxFilter is true)'); 253 | return; 254 | } 255 | } 256 | ifLoggingEnabled('log', 'Sending: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + this.duration + 257 | 'ms.'); 258 | this.url = window.location.hash || window.location.pathname; 259 | var metrics = { 260 | startTime: this.startTime, 261 | duration: this.duration, 262 | url: this.url 263 | }; 264 | if (this.klass && this.klass.length) { 265 | metrics.klass = this.klass; 266 | } 267 | if (this.method && this.method.length) { 268 | metrics.method = this.method; 269 | } 270 | if (this.pattern && this.pattern.length) { 271 | metrics.pattern = this.pattern; 272 | } 273 | var events = []; 274 | for (i = 0; i < this.events.length; i++) { 275 | events.push(this.events[i].serialize(this.startTime)); 276 | } 277 | EmPerfSender.config.send(events, metrics); 278 | }; 279 | return Trace; 280 | })(BaseTrace); 281 | function initialize(userConfig, Ember, $) { 282 | 'use strict'; 283 | if (Ember === void 0) { Ember = window['Ember']; } 284 | if ($ === void 0) { $ = window['jQuery']; } 285 | if (Ember === undefined) { 286 | throw ReferenceError('EmPerfSender cannot find Ember! Check that you have loaded Ember correctly before EmPerfSender!'); 287 | } 288 | else if ($ === undefined) { 289 | throw ReferenceError('EmPerfSender cannot find jQuery! Make sure you have loaded jQuery before Ember and EmPerfSender!'); 290 | } 291 | else { 292 | EmPerfSender.config = $.extend(EmPerfSender.config, userConfig); 293 | ifLoggingEnabled('log', 'Initializing EmPerfSender v' + EmPerfSender.VERSION); 294 | Ember.Route.reopen({ 295 | beforeModel: traceRouteEvent, 296 | afterModel: traceRouteEvent, 297 | enter: traceRouteEvent 298 | }); 299 | Ember.CoreView.reopen({ 300 | /** 301 | * Triggers a named event for the object. Any additional arguments 302 | * will be passed as parameters to the functions that are subscribed to the 303 | * event. 304 | * 305 | * @method trigger 306 | * @param {String} eventName The name of the event 307 | * @param {Object...} args Optional arguments to pass on 308 | */ 309 | trigger: function (eventName) { 310 | var className = this.constructor.toString(), isNotEmberClass = ('Ember' !== className.substr(0, 5) && 311 | // (subclass of Ember...) 312 | 'Ember' !== className.substr(13, 5)), isEvent = this.constructor.prototype.hasOwnProperty(eventName), shouldTrace = isNotEmberClass && isEvent; 313 | if (shouldTrace) { 314 | new Trace(this.constructor.toString(), eventName); 315 | } 316 | return this._super.apply(this, arguments); 317 | } 318 | }); 319 | Ember.ActionHandler.reopen({ 320 | willMergeMixin: function (props) { 321 | var eventName, nextSuper = this._super(props); 322 | if (props._actions) { 323 | for (eventName in props._actions) { 324 | if (props._actions.hasOwnProperty(eventName)) { 325 | props._actions[eventName] = wrapEvent.apply(this, [eventName, props._actions[eventName]]); 326 | } 327 | } 328 | } 329 | else if (props.events) { 330 | for (eventName in props.events) { 331 | if (props.events.hasOwnProperty(eventName)) { 332 | props.events[eventName] = wrapEvent.apply(this, [eventName, props.events[eventName]]); 333 | } 334 | } 335 | } 336 | return nextSuper; 337 | } 338 | }); 339 | Ember.subscribe('render.view', { 340 | before: function (eventName, time, container) { 341 | if (getLastTraceItem()) { 342 | var parentClass = container.object.match(EmPerfSender.config.subclassPattern)[1]; 343 | if ('Ember' !== parentClass.substr(0, 5) && 'LinkView' !== parentClass) { 344 | return new ViewRenderTrace(parentClass); 345 | } 346 | } 347 | }, 348 | after: function (eventName, time, container, tracer) { 349 | if (tracer) { 350 | tracer.stop(); 351 | } 352 | } 353 | }); 354 | Ember.run.backburner.options.onEnd = decorate(Ember.run.backburner.options.onEnd, function (current, next) { 355 | if (!next && EmPerfSender.traceStack.length) { 356 | for (var d = 0; d < EmPerfSender.traceStack.length; d++) { 357 | EmPerfSender.traceStack[d].finalize(); 358 | } 359 | } 360 | }); 361 | aliasMethodChain($, 'ajax', 'instrumentation', decorate($.ajax, ajaxDecorator)); 362 | EmPerfSender.loaded = true; 363 | } 364 | } 365 | EmPerfSender.initialize = initialize; 366 | ; 367 | })(EmPerfSender || (EmPerfSender = {})); 368 | -------------------------------------------------------------------------------- /dist/ember-performance-sender.cjs.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | __.prototype = b.prototype; 5 | d.prototype = new __(); 6 | }; 7 | var EmPerfSender; 8 | (function (EmPerfSender) { 9 | EmPerfSender.VERSION = '1.1.1'; 10 | EmPerfSender.loaded = false; 11 | EmPerfSender.enabled = false; 12 | EmPerfSender.config = { 13 | enableAjaxFilter: false, 14 | enableLogging: true, 15 | minDuration: 50, 16 | log: function (message) { console.log(message); }, 17 | send: function (events, metrics) { console.log('Sending (Dry Run)\n============================\n', metrics); }, 18 | warn: function (message) { console.log('warn: ' + message); }, 19 | error: function (message) { console.log('error: ' + message); }, 20 | subclassPattern: new RegExp('<(?:\\(subclass of )?(.*?)\\)?:.*>') 21 | }; 22 | EmPerfSender.traceStack = []; 23 | function getLastTraceItem() { 24 | return EmPerfSender.traceStack[EmPerfSender.traceStack.length - 1]; 25 | } 26 | function getRoutePattern(EmberRoute) { 27 | try { 28 | var routeName = EmberRoute.get('routeName'), segments = EmberRoute.get('router.router.recognizer.names')[routeName].segments, listOfSegments = []; 29 | for (var i = 0; i < segments.length; i++) { 30 | var segment = null; 31 | try { 32 | segment = segments[i].generate(); 33 | } 34 | catch (err) { 35 | segment = ':' + segments[i].name; 36 | } 37 | if (segment) { 38 | listOfSegments.push(segment); 39 | } 40 | } 41 | return '/' + listOfSegments.join('/'); 42 | } 43 | catch (err) { 44 | return '/'; 45 | } 46 | } 47 | function traceRouteEvent() { 48 | var pattern = getRoutePattern(this), lastTrace = getLastTraceItem(); 49 | if (lastTrace) { 50 | lastTrace.klass = this.constructor.toString(); 51 | lastTrace.method = 'routeTransition'; 52 | lastTrace.pattern = pattern; 53 | } 54 | else { 55 | new Trace(this.constructor.toString(), 'routeTransition', pattern); 56 | } 57 | return this._super.apply(this, arguments); 58 | } 59 | function ifLoggingEnabled(method) { 60 | var args = []; 61 | for (var _i = 1; _i < arguments.length; _i++) { 62 | args[_i - 1] = arguments[_i]; 63 | } 64 | if (EmPerfSender.config.enableLogging && typeof EmPerfSender.config[method] === 'function') { 65 | return EmPerfSender.config[method].apply(this, args); 66 | } 67 | } 68 | function wrap(method) { 69 | var traceItem = getLastTraceItem(); 70 | if (traceItem) { 71 | if (typeof traceItem.pattern === undefined) { 72 | traceItem.klass = this.constructor.toString(); 73 | } 74 | ; 75 | traceItem.method = method; 76 | } 77 | else { 78 | new Trace(this.constructor.toString(), method); 79 | } 80 | } 81 | function wrapEvent(eventName, callback) { 82 | var _this = this; 83 | if ('function' == typeof callback) { 84 | return decorate(callback, function () { 85 | wrap.call(_this, eventName); 86 | }); 87 | } 88 | else { 89 | return callback; 90 | } 91 | } 92 | function decorate(orig, decorator) { 93 | return function () { 94 | var ret; 95 | try { 96 | ret = decorator.apply(this, arguments); 97 | } 98 | catch (e) { 99 | throw e; 100 | } 101 | return (typeof orig === 'function' ? orig : function () { }).apply(this, arguments); 102 | }; 103 | } 104 | function aliasMethodChain($, method, label, wrapper) { 105 | var originalMethod = $[method]; 106 | if ('function' == typeof originalMethod) { 107 | var methodWithout = '__' + method + '_without_' + label + '__', methodWith = '__' + method + '_with_' + label + '__'; 108 | $[methodWithout] = originalMethod; 109 | $[methodWith] = $[method] = wrapper; 110 | } 111 | return $; 112 | } 113 | function ajaxDecorator() { 114 | if (getLastTraceItem()) { 115 | var request = arguments[1] || arguments[0], type = request.type || 'GET', url = request.url || arguments[0]; 116 | if (typeof request === 'string') { 117 | request = {}; 118 | } 119 | var tracer = new AjaxTrace(type, url); 120 | request.success = decorate(request.success, function () { 121 | tracer.trace.resume(); 122 | tracer.stop(); 123 | }); 124 | request.error = decorate(request.error, function () { 125 | tracer.trace.resume(); 126 | tracer.stop(); 127 | }); 128 | request.url = url; 129 | } 130 | } 131 | var BaseTrace = (function () { 132 | function BaseTrace() { 133 | } 134 | BaseTrace.prototype.stop = function () { 135 | if (this.pending) { 136 | this.stopTime = Date.now(); 137 | this.pending = false; 138 | } 139 | else { 140 | ifLoggingEnabled('warn', '[BUG] ' + this.constructor['name'] + ': Attempted to stop a view render twice.'); 141 | } 142 | }; 143 | BaseTrace.prototype.serialize = function (time) { 144 | if (time === void 0) { time = 0; } 145 | var additionalParams = []; 146 | for (var _i = 1; _i < arguments.length; _i++) { 147 | additionalParams[_i - 1] = arguments[_i]; 148 | } 149 | if (this.pending === false) { 150 | var serialized = [this.constructor['name'], this.startTime - time, this.stopTime - this.startTime]; 151 | if (additionalParams.length) { 152 | serialized = serialized.concat(additionalParams); 153 | } 154 | return serialized; 155 | } 156 | }; 157 | return BaseTrace; 158 | })(); 159 | var AjaxTrace = (function (_super) { 160 | __extends(AjaxTrace, _super); 161 | function AjaxTrace(method, url) { 162 | this.method = method; 163 | this.url = url; 164 | this.pending = true; 165 | this.trace = getLastTraceItem(); 166 | if (this.trace) { 167 | this.trace.events.push(this); 168 | } 169 | this.startTime = Date.now(); 170 | _super.call(this); 171 | } 172 | AjaxTrace.prototype.serialize = function (time) { 173 | return _super.prototype.serialize.call(this, time, this.method, this.url); 174 | }; 175 | return AjaxTrace; 176 | })(BaseTrace); 177 | var ViewRenderTrace = (function (_super) { 178 | __extends(ViewRenderTrace, _super); 179 | function ViewRenderTrace(viewName) { 180 | this.viewName = viewName; 181 | this.pending = true; 182 | this.trace = getLastTraceItem(); 183 | if (this.trace) { 184 | this.trace.events.push(this); 185 | } 186 | this.startTime = Date.now(); 187 | _super.call(this); 188 | } 189 | ViewRenderTrace.prototype.serialize = function (time) { 190 | return _super.prototype.serialize.call(this, time, this.viewName); 191 | }; 192 | return ViewRenderTrace; 193 | })(BaseTrace); 194 | var Trace = (function (_super) { 195 | __extends(Trace, _super); 196 | function Trace(klass, method, pattern) { 197 | if (klass.match('Window')) { 198 | throw TypeError(klass + ' is not a valid Trace class'); 199 | } 200 | this.klass = klass; 201 | this.method = method; 202 | this.pattern = pattern; 203 | this.events = []; 204 | this.finalized = false; 205 | this.startTime = Date.now(); 206 | EmPerfSender.traceStack.push(this); 207 | _super.call(this); 208 | } 209 | Trace.prototype.serialize = function (time) { 210 | return _super.prototype.serialize.call(this, time, this.klass, this.method, this.url); 211 | }; 212 | Trace.prototype.pause = function () { 213 | for (var i = 1; i <= EmPerfSender.traceStack.length; i++) { 214 | if (EmPerfSender.traceStack[EmPerfSender.traceStack.length - i] === this) { 215 | EmPerfSender.traceStack.splice(EmPerfSender.traceStack.length - i, 1); 216 | } 217 | } 218 | }; 219 | Trace.prototype.resume = function () { 220 | this.pause(); 221 | EmPerfSender.traceStack.push(this); 222 | }; 223 | Trace.prototype.finalize = function () { 224 | if (this.finalized) { 225 | ifLoggingEnabled('warn', '[BUG] Attempted to finalize a trace twice.'); 226 | return; 227 | } 228 | this.pause(); 229 | for (var i = 0; i < this.events.length; i++) { 230 | if (this.events[i].pending) { 231 | return; 232 | } 233 | } 234 | this.stopTime = Date.now(); 235 | this.finalized = true; 236 | this.duration = this.stopTime - this.startTime; 237 | if (this.duration < (EmPerfSender.config.minDuration || 1)) { 238 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 239 | this.duration + 'ms. (minDuration is ' + EmPerfSender.config.minDuration + 'ms)'); 240 | return; 241 | } 242 | if (EmPerfSender.config.enableAjaxFilter === true) { 243 | var ajaxRequestToTrace = false; 244 | for (i = 0; i < this.events.length; i++) { 245 | if (this.events[i] instanceof AjaxTrace) { 246 | ajaxRequestToTrace = true; 247 | break; 248 | } 249 | } 250 | if (!ajaxRequestToTrace) { 251 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 252 | this.duration + 'ms. (enableAjaxFilter is true)'); 253 | return; 254 | } 255 | } 256 | ifLoggingEnabled('log', 'Sending: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + this.duration + 257 | 'ms.'); 258 | this.url = window.location.hash || window.location.pathname; 259 | var metrics = { 260 | startTime: this.startTime, 261 | duration: this.duration, 262 | url: this.url 263 | }; 264 | if (this.klass && this.klass.length) { 265 | metrics.klass = this.klass; 266 | } 267 | if (this.method && this.method.length) { 268 | metrics.method = this.method; 269 | } 270 | if (this.pattern && this.pattern.length) { 271 | metrics.pattern = this.pattern; 272 | } 273 | var events = []; 274 | for (i = 0; i < this.events.length; i++) { 275 | events.push(this.events[i].serialize(this.startTime)); 276 | } 277 | EmPerfSender.config.send(events, metrics); 278 | }; 279 | return Trace; 280 | })(BaseTrace); 281 | function initialize(userConfig, Ember, $) { 282 | 'use strict'; 283 | if (Ember === void 0) { Ember = window['Ember']; } 284 | if ($ === void 0) { $ = window['jQuery']; } 285 | if (Ember === undefined) { 286 | throw ReferenceError('EmPerfSender cannot find Ember! Check that you have loaded Ember correctly before EmPerfSender!'); 287 | } 288 | else if ($ === undefined) { 289 | throw ReferenceError('EmPerfSender cannot find jQuery! Make sure you have loaded jQuery before Ember and EmPerfSender!'); 290 | } 291 | else { 292 | EmPerfSender.config = $.extend(EmPerfSender.config, userConfig); 293 | ifLoggingEnabled('log', 'Initializing EmPerfSender v' + EmPerfSender.VERSION); 294 | Ember.Route.reopen({ 295 | beforeModel: traceRouteEvent, 296 | afterModel: traceRouteEvent, 297 | enter: traceRouteEvent 298 | }); 299 | Ember.CoreView.reopen({ 300 | /** 301 | * Triggers a named event for the object. Any additional arguments 302 | * will be passed as parameters to the functions that are subscribed to the 303 | * event. 304 | * 305 | * @method trigger 306 | * @param {String} eventName The name of the event 307 | * @param {Object...} args Optional arguments to pass on 308 | */ 309 | trigger: function (eventName) { 310 | var className = this.constructor.toString(), isNotEmberClass = ('Ember' !== className.substr(0, 5) && 311 | // (subclass of Ember...) 312 | 'Ember' !== className.substr(13, 5)), isEvent = this.constructor.prototype.hasOwnProperty(eventName), shouldTrace = isNotEmberClass && isEvent; 313 | if (shouldTrace) { 314 | new Trace(this.constructor.toString(), eventName); 315 | } 316 | return this._super.apply(this, arguments); 317 | } 318 | }); 319 | Ember.ActionHandler.reopen({ 320 | willMergeMixin: function (props) { 321 | var eventName, nextSuper = this._super(props); 322 | if (props._actions) { 323 | for (eventName in props._actions) { 324 | if (props._actions.hasOwnProperty(eventName)) { 325 | props._actions[eventName] = wrapEvent.apply(this, [eventName, props._actions[eventName]]); 326 | } 327 | } 328 | } 329 | else if (props.events) { 330 | for (eventName in props.events) { 331 | if (props.events.hasOwnProperty(eventName)) { 332 | props.events[eventName] = wrapEvent.apply(this, [eventName, props.events[eventName]]); 333 | } 334 | } 335 | } 336 | return nextSuper; 337 | } 338 | }); 339 | Ember.subscribe('render.view', { 340 | before: function (eventName, time, container) { 341 | if (getLastTraceItem()) { 342 | var parentClass = container.object.match(EmPerfSender.config.subclassPattern)[1]; 343 | if ('Ember' !== parentClass.substr(0, 5) && 'LinkView' !== parentClass) { 344 | return new ViewRenderTrace(parentClass); 345 | } 346 | } 347 | }, 348 | after: function (eventName, time, container, tracer) { 349 | if (tracer) { 350 | tracer.stop(); 351 | } 352 | } 353 | }); 354 | Ember.run.backburner.options.onEnd = decorate(Ember.run.backburner.options.onEnd, function (current, next) { 355 | if (!next && EmPerfSender.traceStack.length) { 356 | for (var d = 0; d < EmPerfSender.traceStack.length; d++) { 357 | EmPerfSender.traceStack[d].finalize(); 358 | } 359 | } 360 | }); 361 | aliasMethodChain($, 'ajax', 'instrumentation', decorate($.ajax, ajaxDecorator)); 362 | EmPerfSender.loaded = true; 363 | } 364 | } 365 | EmPerfSender.initialize = initialize; 366 | ; 367 | })(EmPerfSender = exports.EmPerfSender || (exports.EmPerfSender = {})); 368 | -------------------------------------------------------------------------------- /dist/ember-performance-sender.amd.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | __.prototype = b.prototype; 5 | d.prototype = new __(); 6 | }; 7 | define(["require", "exports"], function (require, exports) { 8 | var EmPerfSender; 9 | (function (EmPerfSender) { 10 | EmPerfSender.VERSION = '1.1.1'; 11 | EmPerfSender.loaded = false; 12 | EmPerfSender.enabled = false; 13 | EmPerfSender.config = { 14 | enableAjaxFilter: false, 15 | enableLogging: true, 16 | minDuration: 50, 17 | log: function (message) { console.log(message); }, 18 | send: function (events, metrics) { console.log('Sending (Dry Run)\n============================\n', metrics); }, 19 | warn: function (message) { console.log('warn: ' + message); }, 20 | error: function (message) { console.log('error: ' + message); }, 21 | subclassPattern: new RegExp('<(?:\\(subclass of )?(.*?)\\)?:.*>') 22 | }; 23 | EmPerfSender.traceStack = []; 24 | function getLastTraceItem() { 25 | return EmPerfSender.traceStack[EmPerfSender.traceStack.length - 1]; 26 | } 27 | function getRoutePattern(EmberRoute) { 28 | try { 29 | var routeName = EmberRoute.get('routeName'), segments = EmberRoute.get('router.router.recognizer.names')[routeName].segments, listOfSegments = []; 30 | for (var i = 0; i < segments.length; i++) { 31 | var segment = null; 32 | try { 33 | segment = segments[i].generate(); 34 | } 35 | catch (err) { 36 | segment = ':' + segments[i].name; 37 | } 38 | if (segment) { 39 | listOfSegments.push(segment); 40 | } 41 | } 42 | return '/' + listOfSegments.join('/'); 43 | } 44 | catch (err) { 45 | return '/'; 46 | } 47 | } 48 | function traceRouteEvent() { 49 | var pattern = getRoutePattern(this), lastTrace = getLastTraceItem(); 50 | if (lastTrace) { 51 | lastTrace.klass = this.constructor.toString(); 52 | lastTrace.method = 'routeTransition'; 53 | lastTrace.pattern = pattern; 54 | } 55 | else { 56 | new Trace(this.constructor.toString(), 'routeTransition', pattern); 57 | } 58 | return this._super.apply(this, arguments); 59 | } 60 | function ifLoggingEnabled(method) { 61 | var args = []; 62 | for (var _i = 1; _i < arguments.length; _i++) { 63 | args[_i - 1] = arguments[_i]; 64 | } 65 | if (EmPerfSender.config.enableLogging && typeof EmPerfSender.config[method] === 'function') { 66 | return EmPerfSender.config[method].apply(this, args); 67 | } 68 | } 69 | function wrap(method) { 70 | var traceItem = getLastTraceItem(); 71 | if (traceItem) { 72 | if (typeof traceItem.pattern === undefined) { 73 | traceItem.klass = this.constructor.toString(); 74 | } 75 | ; 76 | traceItem.method = method; 77 | } 78 | else { 79 | new Trace(this.constructor.toString(), method); 80 | } 81 | } 82 | function wrapEvent(eventName, callback) { 83 | var _this = this; 84 | if ('function' == typeof callback) { 85 | return decorate(callback, function () { 86 | wrap.call(_this, eventName); 87 | }); 88 | } 89 | else { 90 | return callback; 91 | } 92 | } 93 | function decorate(orig, decorator) { 94 | return function () { 95 | var ret; 96 | try { 97 | ret = decorator.apply(this, arguments); 98 | } 99 | catch (e) { 100 | throw e; 101 | } 102 | return (typeof orig === 'function' ? orig : function () { }).apply(this, arguments); 103 | }; 104 | } 105 | function aliasMethodChain($, method, label, wrapper) { 106 | var originalMethod = $[method]; 107 | if ('function' == typeof originalMethod) { 108 | var methodWithout = '__' + method + '_without_' + label + '__', methodWith = '__' + method + '_with_' + label + '__'; 109 | $[methodWithout] = originalMethod; 110 | $[methodWith] = $[method] = wrapper; 111 | } 112 | return $; 113 | } 114 | function ajaxDecorator() { 115 | if (getLastTraceItem()) { 116 | var request = arguments[1] || arguments[0], type = request.type || 'GET', url = request.url || arguments[0]; 117 | if (typeof request === 'string') { 118 | request = {}; 119 | } 120 | var tracer = new AjaxTrace(type, url); 121 | request.success = decorate(request.success, function () { 122 | tracer.trace.resume(); 123 | tracer.stop(); 124 | }); 125 | request.error = decorate(request.error, function () { 126 | tracer.trace.resume(); 127 | tracer.stop(); 128 | }); 129 | request.url = url; 130 | } 131 | } 132 | var BaseTrace = (function () { 133 | function BaseTrace() { 134 | } 135 | BaseTrace.prototype.stop = function () { 136 | if (this.pending) { 137 | this.stopTime = Date.now(); 138 | this.pending = false; 139 | } 140 | else { 141 | ifLoggingEnabled('warn', '[BUG] ' + this.constructor['name'] + ': Attempted to stop a view render twice.'); 142 | } 143 | }; 144 | BaseTrace.prototype.serialize = function (time) { 145 | if (time === void 0) { time = 0; } 146 | var additionalParams = []; 147 | for (var _i = 1; _i < arguments.length; _i++) { 148 | additionalParams[_i - 1] = arguments[_i]; 149 | } 150 | if (this.pending === false) { 151 | var serialized = [this.constructor['name'], this.startTime - time, this.stopTime - this.startTime]; 152 | if (additionalParams.length) { 153 | serialized = serialized.concat(additionalParams); 154 | } 155 | return serialized; 156 | } 157 | }; 158 | return BaseTrace; 159 | })(); 160 | var AjaxTrace = (function (_super) { 161 | __extends(AjaxTrace, _super); 162 | function AjaxTrace(method, url) { 163 | this.method = method; 164 | this.url = url; 165 | this.pending = true; 166 | this.trace = getLastTraceItem(); 167 | if (this.trace) { 168 | this.trace.events.push(this); 169 | } 170 | this.startTime = Date.now(); 171 | _super.call(this); 172 | } 173 | AjaxTrace.prototype.serialize = function (time) { 174 | return _super.prototype.serialize.call(this, time, this.method, this.url); 175 | }; 176 | return AjaxTrace; 177 | })(BaseTrace); 178 | var ViewRenderTrace = (function (_super) { 179 | __extends(ViewRenderTrace, _super); 180 | function ViewRenderTrace(viewName) { 181 | this.viewName = viewName; 182 | this.pending = true; 183 | this.trace = getLastTraceItem(); 184 | if (this.trace) { 185 | this.trace.events.push(this); 186 | } 187 | this.startTime = Date.now(); 188 | _super.call(this); 189 | } 190 | ViewRenderTrace.prototype.serialize = function (time) { 191 | return _super.prototype.serialize.call(this, time, this.viewName); 192 | }; 193 | return ViewRenderTrace; 194 | })(BaseTrace); 195 | var Trace = (function (_super) { 196 | __extends(Trace, _super); 197 | function Trace(klass, method, pattern) { 198 | if (klass.match('Window')) { 199 | throw TypeError(klass + ' is not a valid Trace class'); 200 | } 201 | this.klass = klass; 202 | this.method = method; 203 | this.pattern = pattern; 204 | this.events = []; 205 | this.finalized = false; 206 | this.startTime = Date.now(); 207 | EmPerfSender.traceStack.push(this); 208 | _super.call(this); 209 | } 210 | Trace.prototype.serialize = function (time) { 211 | return _super.prototype.serialize.call(this, time, this.klass, this.method, this.url); 212 | }; 213 | Trace.prototype.pause = function () { 214 | for (var i = 1; i <= EmPerfSender.traceStack.length; i++) { 215 | if (EmPerfSender.traceStack[EmPerfSender.traceStack.length - i] === this) { 216 | EmPerfSender.traceStack.splice(EmPerfSender.traceStack.length - i, 1); 217 | } 218 | } 219 | }; 220 | Trace.prototype.resume = function () { 221 | this.pause(); 222 | EmPerfSender.traceStack.push(this); 223 | }; 224 | Trace.prototype.finalize = function () { 225 | if (this.finalized) { 226 | ifLoggingEnabled('warn', '[BUG] Attempted to finalize a trace twice.'); 227 | return; 228 | } 229 | this.pause(); 230 | for (var i = 0; i < this.events.length; i++) { 231 | if (this.events[i].pending) { 232 | return; 233 | } 234 | } 235 | this.stopTime = Date.now(); 236 | this.finalized = true; 237 | this.duration = this.stopTime - this.startTime; 238 | if (this.duration < (EmPerfSender.config.minDuration || 1)) { 239 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 240 | this.duration + 'ms. (minDuration is ' + EmPerfSender.config.minDuration + 'ms)'); 241 | return; 242 | } 243 | if (EmPerfSender.config.enableAjaxFilter === true) { 244 | var ajaxRequestToTrace = false; 245 | for (i = 0; i < this.events.length; i++) { 246 | if (this.events[i] instanceof AjaxTrace) { 247 | ajaxRequestToTrace = true; 248 | break; 249 | } 250 | } 251 | if (!ajaxRequestToTrace) { 252 | ifLoggingEnabled('log', 'Dropped: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + 253 | this.duration + 'ms. (enableAjaxFilter is true)'); 254 | return; 255 | } 256 | } 257 | ifLoggingEnabled('log', 'Sending: ' + this.klass + '.' + this.method + ' (' + this.pattern + '), took ' + this.duration + 258 | 'ms.'); 259 | this.url = window.location.hash || window.location.pathname; 260 | var metrics = { 261 | startTime: this.startTime, 262 | duration: this.duration, 263 | url: this.url 264 | }; 265 | if (this.klass && this.klass.length) { 266 | metrics.klass = this.klass; 267 | } 268 | if (this.method && this.method.length) { 269 | metrics.method = this.method; 270 | } 271 | if (this.pattern && this.pattern.length) { 272 | metrics.pattern = this.pattern; 273 | } 274 | var events = []; 275 | for (i = 0; i < this.events.length; i++) { 276 | events.push(this.events[i].serialize(this.startTime)); 277 | } 278 | EmPerfSender.config.send(events, metrics); 279 | }; 280 | return Trace; 281 | })(BaseTrace); 282 | function initialize(userConfig, Ember, $) { 283 | 'use strict'; 284 | if (Ember === void 0) { Ember = window['Ember']; } 285 | if ($ === void 0) { $ = window['jQuery']; } 286 | if (Ember === undefined) { 287 | throw ReferenceError('EmPerfSender cannot find Ember! Check that you have loaded Ember correctly before EmPerfSender!'); 288 | } 289 | else if ($ === undefined) { 290 | throw ReferenceError('EmPerfSender cannot find jQuery! Make sure you have loaded jQuery before Ember and EmPerfSender!'); 291 | } 292 | else { 293 | EmPerfSender.config = $.extend(EmPerfSender.config, userConfig); 294 | ifLoggingEnabled('log', 'Initializing EmPerfSender v' + EmPerfSender.VERSION); 295 | Ember.Route.reopen({ 296 | beforeModel: traceRouteEvent, 297 | afterModel: traceRouteEvent, 298 | enter: traceRouteEvent 299 | }); 300 | Ember.CoreView.reopen({ 301 | /** 302 | * Triggers a named event for the object. Any additional arguments 303 | * will be passed as parameters to the functions that are subscribed to the 304 | * event. 305 | * 306 | * @method trigger 307 | * @param {String} eventName The name of the event 308 | * @param {Object...} args Optional arguments to pass on 309 | */ 310 | trigger: function (eventName) { 311 | var className = this.constructor.toString(), isNotEmberClass = ('Ember' !== className.substr(0, 5) && 312 | // (subclass of Ember...) 313 | 'Ember' !== className.substr(13, 5)), isEvent = this.constructor.prototype.hasOwnProperty(eventName), shouldTrace = isNotEmberClass && isEvent; 314 | if (shouldTrace) { 315 | new Trace(this.constructor.toString(), eventName); 316 | } 317 | return this._super.apply(this, arguments); 318 | } 319 | }); 320 | Ember.ActionHandler.reopen({ 321 | willMergeMixin: function (props) { 322 | var eventName, nextSuper = this._super(props); 323 | if (props._actions) { 324 | for (eventName in props._actions) { 325 | if (props._actions.hasOwnProperty(eventName)) { 326 | props._actions[eventName] = wrapEvent.apply(this, [eventName, props._actions[eventName]]); 327 | } 328 | } 329 | } 330 | else if (props.events) { 331 | for (eventName in props.events) { 332 | if (props.events.hasOwnProperty(eventName)) { 333 | props.events[eventName] = wrapEvent.apply(this, [eventName, props.events[eventName]]); 334 | } 335 | } 336 | } 337 | return nextSuper; 338 | } 339 | }); 340 | Ember.subscribe('render.view', { 341 | before: function (eventName, time, container) { 342 | if (getLastTraceItem()) { 343 | var parentClass = container.object.match(EmPerfSender.config.subclassPattern)[1]; 344 | if ('Ember' !== parentClass.substr(0, 5) && 'LinkView' !== parentClass) { 345 | return new ViewRenderTrace(parentClass); 346 | } 347 | } 348 | }, 349 | after: function (eventName, time, container, tracer) { 350 | if (tracer) { 351 | tracer.stop(); 352 | } 353 | } 354 | }); 355 | Ember.run.backburner.options.onEnd = decorate(Ember.run.backburner.options.onEnd, function (current, next) { 356 | if (!next && EmPerfSender.traceStack.length) { 357 | for (var d = 0; d < EmPerfSender.traceStack.length; d++) { 358 | EmPerfSender.traceStack[d].finalize(); 359 | } 360 | } 361 | }); 362 | aliasMethodChain($, 'ajax', 'instrumentation', decorate($.ajax, ajaxDecorator)); 363 | EmPerfSender.loaded = true; 364 | } 365 | } 366 | EmPerfSender.initialize = initialize; 367 | ; 368 | })(EmPerfSender = exports.EmPerfSender || (exports.EmPerfSender = {})); 369 | }); 370 | --------------------------------------------------------------------------------