Index: openacs-4/packages/boomerang/tcl/boomerang-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/boomerang/tcl/boomerang-procs.tcl,v diff -u -N -r1.13.2.21 -r1.13.2.22 --- openacs-4/packages/boomerang/tcl/boomerang-procs.tcl 16 Aug 2022 17:47:50 -0000 1.13.2.21 +++ openacs-4/packages/boomerang/tcl/boomerang-procs.tcl 24 Aug 2022 16:50:31 -0000 1.13.2.22 @@ -152,14 +152,18 @@ # dict set entries clientip $peeraddr + if {![dict exists $entries rt.tstart] && [dict exists $entries nt_nav_st]} { + dict set entries rt.tstart [dict get $entries nt_nav_st] + } + if {[dict exists $entries err]} { ad_log warning "boomerang: returned error: [dict get $entries err]\n\ Request-info:\n[util::request_info -with_headers]" set record 0 } elseif {![dict exists $entries rt.tstart]} { ns_log notice "boomerang: no rt.tstart value in $entries" set record 0 - } elseif {![string is integer -strict [dict get $entries rt.tstart]]} { + } elseif {![string is entier -strict [dict get $entries rt.tstart]]} { ns_log notice "boomerang: rt.tstart is not a valid timestamp <[dict get $entries rt.tstart]>" set record 0 } else { @@ -383,7 +387,6 @@ set version_info [version_info] set prefix [dict get $version_info prefix] - ns_log notice "XXXX boomerang prefix <$prefix>" foreach jsFile [dict get $version_info jsFiles] { template::head::add_javascript -src ${prefix}/$jsFile } @@ -409,12 +412,17 @@ # "plugins/usertiming.js", # "plugins/mq.js" # - template::head::add_javascript -src ${prefix}/plugins-$version/rt.js + template::head::add_javascript -src ${prefix}/plugins-$version/navtiming.js + #template::head::add_javascript -src ${prefix}/plugins-$version/rt.js template::head::add_javascript -order 2 -script [subst { BOOMR.init({ beacon_url: "$beaconURL", - log: null + log: null, + NavigationTiming: { + enabled: true, + clearOnBeacon: true + } }); }] } Index: openacs-4/packages/boomerang/www/resources/plugins-1.737.0/navtiming.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/boomerang/www/resources/plugins-1.737.0/Attic/navtiming.js,v diff -u -N --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/boomerang/www/resources/plugins-1.737.0/navtiming.js 24 Aug 2022 16:50:31 -0000 1.1.2.1 @@ -0,0 +1,456 @@ +/* + * Copyright (c), Buddy Brewer. + */ +/** + * The Navigation Timing plugin collects performance metrics collected by modern + * user agents that support the W3C [NavigationTiming]{@link http://www.w3.org/TR/navigation-timing/} + * specification. + * + * This plugin also adds similar [ResourceTiming]{@link https://www.w3.org/TR/resource-timing-1/} + * metrics for any XHR beacons. + * + * For information on how to include this plugin, see the {@tutorial building} tutorial. + * + * ## Beacon Parameters + * + * All beacon parameters are prefixed with `nt_`. + * + * This plugin adds the following parameters to the beacon for Page Loads: + * + * * `nt_red_cnt`: `performance.navigation.redirectCount` + * * `nt_nav_type`: `performance.navigation.type` + * * `nt_nav_st`: `performance.timing.navigationStart` + * * `nt_red_st`: `performance.timing.redirectStart` + * * `nt_red_end`: `performance.timing.redirectEnd` + * * `nt_fet_st`: `performance.timing.fetchStart` + * * `nt_dns_st`: `performance.timing.domainLookupStart` + * * `nt_dns_end`: `performance.timing.domainLookupEnd` + * * `nt_con_st`: `performance.timing.connectStart` + * * `nt_con_end`: `performance.timing.connectEnd` + * * `nt_req_st`: `performance.timing.requestStart` + * * `nt_res_st`: `performance.timing.responseStart` + * * `nt_res_end`: `performance.timing.responseEnd` + * * `nt_domloading`: `performance.timing.domLoading` + * * `nt_domint`: `performance.timing.domInteractive` + * * `nt_domcontloaded_st`: `performance.timing.domContentLoadedEventStart` + * * `nt_domcontloaded_end`: `performance.timing.domContentLoadedEventEnd` + * * `nt_domcomp`: `performance.timing.domComplete` + * * `nt_load_st`: `performance.timing.loadEventStart` + * * `nt_load_end`: `performance.timing.loadEventEnd` + * * `nt_unload_st`: `performance.timing.unloadEventStart` + * * `nt_unload_end`: `performance.timing.unloadEventEnd` + * * `nt_ssl_st`: `performance.timing.secureConnectionStart` + * * `nt_spdy`: `1` if page was loaded over SPDY, `0` otherwise. Only available + * in Chrome when it _doesn't_ support NavigationTiming2. If NavigationTiming2 + * is supported, `nt_protocol` will be added instead. + * * `nt_first_paint`: The time when the first paint happened. If the browser + * supports the Paint Timing API, this is the `first-paint` time in milliseconds + * since the epoch. Else, on Internet Explorer, this is the `msFirstPaint` + * value, in milliseconds since the epoch. On Chrome, this is using + * `loadTimes().firstPaintTime` and is converted from seconds.microseconds + * into milliseconds since the epoch. + * * `nt_cinf`: Chrome `chrome.loadTimes().connectionInfo`. Only available + * in Chrome when it _doesn't_ support NavigationTiming2. If NavigationTiming2 + * is supported, `nt_protocol` will be added instead. + * * `nt_protocol`: NavigationTiming2's `nextHopProtocol` + * * `nt_bad`: If we detected that any NavigationTiming metrics looked odd, + * such as `responseEnd` in the far future or `fetchStart` before `navigationStart`. + * * `nt_worker_start`: NavigationTiming2 `workerStart` + * * `nt_enc_size`: NavigationTiming2 `encodedBodySize` + * * `nt_dec_size`: NavigationTiming2 `decodedBodySize` + * * `nt_trn_size`: NavigationTiming2 `transferSize` + * + * For XHR beacons, the following parameters are added (via ResourceTiming): + * + * * `nt_red_st`: `redirectStart` + * * `nt_red_end`: `redirectEnd` + * * `nt_fet_st`: `fetchStart` + * * `nt_dns_st`: `domainLookupStart` + * * `nt_dns_end`: `domainLookupEnd` + * * `nt_con_st`: `connectStart` + * * `nt_con_end`: `connectEnd` + * * `nt_req_st`: `requestStart` + * * `nt_res_st`: `responseStart` + * * `nt_res_end`: `responseEnd` + * * `nt_load_st`: `loadEventStart` + * * `nt_load_end`: `loadEventEnd` + * * `nt_ssl_st`: `secureConnectionStart` + * + * @see {@link http://www.w3.org/TR/navigation-timing/} + * @see {@link https://www.w3.org/TR/resource-timing-1/} + * @class BOOMR.plugins.NavigationTiming + */ +(function() { + BOOMR = window.BOOMR || {}; + BOOMR.plugins = BOOMR.plugins || {}; + + if (BOOMR.plugins.NavigationTiming) { + return; + } + + /** + * Calculates a NavigationTiming timestamp for the beacon, in milliseconds + * since the Unix Epoch. + * + * The offset should be 0 if using a timestamp from performance.timing (which + * are already in milliseconds since Unix Epoch), or the value of navigationStart + * if using getEntriesByType("navigation") (which are DOMHighResTimestamps). + * + * The number is stripped of any decimals. + * + * @param {number} offset navigationStart offset (0 if using NavTiming1) + * @param {number} val DOMHighResTimestamp + * + * @returns {number} Timestamp for beacon + */ + function calcNavTimingTimestamp(offset, val) { + if (typeof val !== "number" || val === 0) { + return undefined; + } + + return Math.floor((offset || 0) + val); + } + + // A private object to encapsulate all your implementation details + var impl = { + complete: false, + fullySent: false, + sendBeacon: function() { + this.complete = true; + BOOMR.sendBeacon(); + }, + xhr_done: function(edata) { + var p; + + if (edata && edata.initiator === "spa_hard") { + // Single Page App - Hard refresh: Send page's NavigationTiming data, if + // available. + impl.done(edata); + return; + } + else if (edata && edata.initiator === "spa") { + // Single Page App - Soft refresh: The original hard navigation is no longer + // relevant for this soft refresh, nor is the "URL" for this page, so don't + // add NavigationTiming or ResourceTiming metrics. + impl.sendBeacon(); + return; + } + + var w = BOOMR.window, res, data = {}, k; + + if (!edata) { + return; + } + + if (edata.data) { + edata = edata.data; + } + + p = BOOMR.getPerformance(); + + // if we previously saved the correct ResourceTiming entry, use it + if (p && edata.restiming) { + data = { + nt_red_st: edata.restiming.redirectStart, + nt_red_end: edata.restiming.redirectEnd, + nt_fet_st: edata.restiming.fetchStart, + nt_dns_st: edata.restiming.domainLookupStart, + nt_dns_end: edata.restiming.domainLookupEnd, + nt_con_st: edata.restiming.connectStart, + nt_con_end: edata.restiming.connectEnd, + nt_req_st: edata.restiming.requestStart, + nt_res_st: edata.restiming.responseStart, + nt_res_end: edata.restiming.responseEnd + }; + + if (edata.restiming.secureConnectionStart) { + // secureConnectionStart is OPTIONAL in the spec + data.nt_ssl_st = edata.restiming.secureConnectionStart; + } + + for (k in data) { + if (data.hasOwnProperty(k) && data[k]) { + data[k] += p.timing.navigationStart; + + // don't need to send microseconds + data[k] = Math.floor(data[k]); + } + } + } + + if (edata.timing) { + res = edata.timing; + if (!data.nt_req_st) { + // requestStart will be 0 if Timing-Allow-Origin header isn't set on the xhr response + data.nt_req_st = res.requestStart; + } + if (!data.nt_res_st) { + // responseStart will be 0 if Timing-Allow-Origin header isn't set on the xhr response + data.nt_res_st = res.responseStart; + } + if (!data.nt_res_end) { + data.nt_res_end = res.responseEnd; + } + data.nt_domint = res.domInteractive; + data.nt_domcomp = res.domComplete; + data.nt_load_st = res.loadEventEnd; + data.nt_load_end = res.loadEventEnd; + } + + for (k in data) { + if (data.hasOwnProperty(k) && !data[k]) { + delete data[k]; + } + } + + BOOMR.addVar(data, undefined, true); + + impl.sendBeacon(); + }, + + done: function() { + var w = BOOMR.window, p, pn, chromeTimes, pt, data = {}, offset = 0, i, + paintTiming, paintTimingSupported = false, k; + + if (this.complete) { + return this; + } + + p = BOOMR.getPerformance(); + + if (p) { + if (typeof p.getEntriesByType === "function") { + pt = p.getEntriesByType("navigation"); + if (pt && pt.length) { + BOOMR.info("This user agent supports NavigationTiming2", "nt"); + + pt = pt[0]; + + // ensure DOMHighResTimestamps are added to navigationStart + offset = p.timing ? p.timing.navigationStart : 0; + } + else { + pt = undefined; + } + } + + if (!pt && p.timing) { + BOOMR.info("This user agent supports NavigationTiming", "nt"); + pt = p.timing; + } + + if (pt) { + data = { + // start is `navigationStart` on .timing, `startTime` is always 0 on timeline entry + nt_nav_st: p.timing ? p.timing.navigationStart : 0, + + // all other entries have the same name on .timing vs timeline entry + nt_red_st: calcNavTimingTimestamp(offset, pt.redirectStart), + nt_red_end: calcNavTimingTimestamp(offset, pt.redirectEnd), + nt_fet_st: calcNavTimingTimestamp(offset, pt.fetchStart), + nt_dns_st: calcNavTimingTimestamp(offset, pt.domainLookupStart), + nt_dns_end: calcNavTimingTimestamp(offset, pt.domainLookupEnd), + nt_con_st: calcNavTimingTimestamp(offset, pt.connectStart), + nt_con_end: calcNavTimingTimestamp(offset, pt.connectEnd), + nt_req_st: calcNavTimingTimestamp(offset, pt.requestStart), + nt_res_st: calcNavTimingTimestamp(offset, pt.responseStart), + nt_res_end: calcNavTimingTimestamp(offset, pt.responseEnd), + nt_domloading: calcNavTimingTimestamp(offset, pt.domLoading), + nt_domint: calcNavTimingTimestamp(offset, pt.domInteractive), + nt_domcontloaded_st: calcNavTimingTimestamp(offset, pt.domContentLoadedEventStart), + nt_domcontloaded_end: calcNavTimingTimestamp(offset, pt.domContentLoadedEventEnd), + nt_domcomp: calcNavTimingTimestamp(offset, pt.domComplete), + nt_load_st: calcNavTimingTimestamp(offset, pt.loadEventStart), + nt_load_end: calcNavTimingTimestamp(offset, pt.loadEventEnd), + nt_unload_st: calcNavTimingTimestamp(offset, pt.unloadEventStart), + nt_unload_end: calcNavTimingTimestamp(offset, pt.unloadEventEnd) + }; + + // domLoading doesn't exist on NavigationTiming2, so fetch it + // from performance.timing if available. + if (!data.nt_domloading && p && p.timing && p.timing.domLoading) { + // value on performance.timing will be in Unix Epoch milliseconds + data.nt_domloading = p.timing.domLoading; + } + + if (pt.secureConnectionStart) { + // secureConnectionStart is OPTIONAL in the spec + data.nt_ssl_st = calcNavTimingTimestamp(offset, pt.secureConnectionStart); + } + + if (p.timing && p.timing.msFirstPaint) { + // msFirstPaint is IE9+ http://msdn.microsoft.com/en-us/library/ff974719 + // and is in Unix Epoch format + data.nt_first_paint = p.timing.msFirstPaint; + } + + if (pt.workerStart) { + // ServiceWorker time + data.nt_worker_start = calcNavTimingTimestamp(offset, pt.workerStart); + } + + // Need to check both decodedSize and transferSize as + // transferSize is 0 for cached responses and + // decodedSize is 0 for empty responses (eg: beacons, 204s, etc.) + if (pt.decodedBodySize || pt.transferSize) { + data.nt_enc_size = pt.encodedBodySize; + data.nt_dec_size = pt.decodedBodySize; + data.nt_trn_size = pt.transferSize; + } + + if (pt.nextHopProtocol) { + data.nt_protocol = pt.nextHopProtocol; + } + } + + // + // Get First Paint from Paint Timing API + // https://www.w3.org/TR/paint-timing/ + // + if (!data.nt_first_paint && BOOMR.plugins.PaintTiming) { + paintTimingSupported = BOOMR.plugins.PaintTiming.is_supported(); + + paintTiming = BOOMR.plugins.PaintTiming.getTimingFor("first-paint"); + + if (paintTiming) { + data.nt_first_paint = calcNavTimingTimestamp(offset, paintTiming); + } + } + + // + // Chrome provides window.chrome.loadTimes(), but this is deprecated + // in Chrome 64+ and will be removed at some point. The data it + // provides may be available in more modern performance APIs: + // + // * .connectionInfo (nt_cinf): Navigation Timing 2 nextHopProtocol + // * .wasFetchedViaSpdy (nt_spdy): Could be calculated via above, + // so we don't need to add if it's not available directly + // * .firstPaintTime (nt_first_paint): Paint Timing's first-paint + // + // If we've already queried that data, don't also query + // loadTimes() as it will generate a console warning. + // + if ((!data.nt_protocol || !data.nt_first_paint) && + (!pt || pt.nextHopProtocol !== "") && + !paintTimingSupported && + w.chrome && + typeof w.chrome.loadTimes === "function") { + chromeTimes = w.chrome.loadTimes(); + if (chromeTimes) { + data.nt_spdy = (chromeTimes.wasFetchedViaSpdy ? 1 : 0); + data.nt_cinf = chromeTimes.connectionInfo; + + // Chrome firstPaintTime is in seconds.microseconds, so + // we need to multiply it by 1000 to be consistent with + // msFirstPaint and other NavigationTiming timestamps that + // are in milliseconds.microseconds. + if (typeof chromeTimes.firstPaintTime === "number" && chromeTimes.firstPaintTime !== 0) { + data.nt_first_paint = Math.round(chromeTimes.firstPaintTime * 1000); + } + } + } + + // + // Navigation Type and Redirect Count + // + if (p.navigation) { + pn = p.navigation; + + data.nt_red_cnt = pn.redirectCount; + data.nt_nav_type = pn.type; + } + + // Remove any properties that are undefined + for (k in data) { + if (data.hasOwnProperty(k) && data[k] === undefined) { + delete data[k]; + } + } + + BOOMR.addVar(data, undefined, true); + + // + // Basic browser bug detection for known cases where NavigationTiming + // timestamps might not be trusted. + // + if (pt && ( + (pt.requestStart && pt.navigationStart && pt.requestStart < pt.navigationStart) || + (pt.responseStart && pt.navigationStart && pt.responseStart < pt.navigationStart) || + (pt.responseStart && pt.fetchStart && pt.responseStart < pt.fetchStart) || + (pt.navigationStart && pt.fetchStart < pt.navigationStart) || + (pt.responseEnd && pt.responseEnd > BOOMR.now() + 8.64e+7) + )) { + BOOMR.addVar("nt_bad", 1, true); + } + + if (data.nt_load_end > 0) { + this.fullySent = true; + } + } + + impl.sendBeacon(); + }, + + clear: function(edata) { + // Allow the data to go out on both an Early beacon and the regular Page Load beacon, + // but after that, if we ever sent the full data, we're complete for all times. + this.complete = !(edata && edata.early) && this.fullySent; + }, + + prerenderToVisible: function() { + // ensure we add our data to the beacon even if we had added it + // during prerender (in case another beacon went out in between) + this.complete = false; + + // add our data to the beacon + this.done(); + }, + + onBeforeEarlyBeacon: function(edata) { + // Add our data to the early beacon + if (!edata /* dom_loaded */ || + typeof edata.initiator === "undefined" /* onconfig */ || + edata.initiator === "spa_hard") { + this.done(); + } + } + }; + + // + // Exports + // + BOOMR.plugins.NavigationTiming = { + /** + * Initializes the plugin. + * + * This plugin does not have any configuration. + * @returns {@link BOOMR.plugins.NavigationTiming} The NavigationTiming plugin for chaining + * @memberof BOOMR.plugins.NavigationTiming + */ + init: function() { + if (!impl.initialized) { + // we'll fire on whichever happens first + BOOMR.subscribe("page_ready", impl.done, null, impl); + BOOMR.subscribe("prerender_to_visible", impl.prerenderToVisible, null, impl); + BOOMR.subscribe("before_early_beacon", impl.onBeforeEarlyBeacon, null, impl); + BOOMR.subscribe("xhr_load", impl.xhr_done, null, impl); + BOOMR.subscribe("before_unload", impl.done, null, impl); + BOOMR.subscribe("beacon", impl.clear, null, impl); + + impl.initialized = true; + } + return this; + }, + + /** + * Whether or not this plugin is complete + * + * @returns {boolean} `true` if the plugin is complete + * @memberof BOOMR.plugins.NavigationTiming + */ + is_complete: function() { + return true; + } + }; + +}());