Skip to content

Commit

Permalink
Angular: Track navigations when only a $locationChangeStart fires
Browse files Browse the repository at this point in the history
  • Loading branch information
nicjansma committed Apr 3, 2018
1 parent e815d9c commit 031c1fb
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 55 deletions.
44 changes: 41 additions & 3 deletions plugins/angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
(function() {
var hooked = false,
enabled = true,
hadMissedRouteChange = false;
hadMissedRouteChange = false,
lastRouteChangeTime = 0,
locationChangeTrigger = false;

if (BOOMR.plugins.Angular || typeof BOOMR.plugins.SPA === "undefined") {
return;
Expand Down Expand Up @@ -76,6 +78,36 @@

log("Startup");

/**
* Fires the SPA route_change event.
*
* If it's been over 50ms since the last route change, fire a new
* route change event. Several Angular events may call this function that
* trigger off each other (e.g. $routeChangeStart from $locationChangeStart),
* so this combines them into a single route change.
*
* We also want $routeChangeStart/$stateChangeStart to trigger the route_change()
* if possible because those have arguments that we'll pass to the routeFilter
* if specified.
*
* To do this, $locationChangeStart sets a timeout before calling this
* that will be cleared by the $routeChangeStart/$stateChangeStart.
*/
function fireRouteChange() {
var now = BOOMR.now();

if (now - lastRouteChangeTime > 50) {
BOOMR.plugins.SPA.route_change.call(null, null, arguments);
}

lastRouteChangeTime = now;

// clear any $locationChangeStart triggers since we've handled it
// either via $routeChangeStart or $stateChangeStart
clearTimeout(locationChangeTrigger);
locationChangeTrigger = false;
}

//
// Traditional Angular Router events
//
Expand All @@ -91,7 +123,7 @@

log("$routeChangeStart: " + (next ? next.templateUrl : ""));

BOOMR.plugins.SPA.route_change(event, next, current);
fireRouteChange(event, next, current);
});

// Listen for $locationChangeStart to know the new URL when the route changes
Expand All @@ -104,6 +136,12 @@

BOOMR.fireEvent("spa_init", [BOOMR.plugins.SPA.current_spa_nav(), newState]);
BOOMR.plugins.SPA.last_location(newState);

// Fire a route change (on a short delay) after this callback in case
// $routeChangeStart never fires. We'd prefer to use $routeChangeStart's
// arguments (next, current) for any routeFilter, so use a setTimeout
// that may be cancelled by the $routeChangeStart/$stateChangeStart event.
locationChangeTrigger = setTimeout(fireRouteChange, 0);
});

//
Expand All @@ -117,7 +155,7 @@

log("$stateChangeStart: " + toState);

BOOMR.plugins.SPA.route_change(event, toState, toParams, fromState, fromParams);
fireRouteChange(event, toState, toParams, fromState, fromParams);
});

$rootScope.$on("$stateChangeSuccess", function(event, toState, toParams, fromState, fromParams) {
Expand Down
5 changes: 3 additions & 2 deletions plugins/spa.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,13 @@
* Called by a framework when a route change has happened
*
* @param {function} onComplete Called on completion
* @param {object[]} routeFilterArgs Route Filter arguments
*/
route_change: function(onComplete) {
route_change: function(onComplete, routeFilterArgs) {
// if we have a routeFilter, see if they want to track this route
if (routeFilter) {
try {
if (!routeFilter.apply(null, arguments)) {
if (!routeFilter.apply(null, routeFilterArgs)) {
return;
}
}
Expand Down
34 changes: 34 additions & 0 deletions tests/page-templates/05-angular/108-location-change-only.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<%= header %>
<%= boomerangScript %>
<base href="/pages/05-angular/108-location-change-only.html" />
<script src="../../vendor/angular/angular.js"></script>
<!-- no angular-route -->
<script src="../../vendor/angular-resource/angular-resource.js"></script>
<script>
window.angular_imgs = [3000];

window.angular_html5_mode = true;

// view a widget then come back so debugging (F5) is easier
window.angular_nav_routes = ["/nothing", "108-location-change-only.html"];

window.angular_only_change_loc = true;
window.angular_no_route = true;
</script>
<script src="support/app.js"></script>
<div ng-app="app">
<div ng-view>
</div>
</div>
<script src="108-location-change-only.js" type="text/javascript"></script>
<script>
BOOMR_test.init({
testAfterOnBeacon: 3,
Angular: {
enabled: true
},
instrument_xhr: true,
autorun: false
});
</script>
<%= footer %>
138 changes: 138 additions & 0 deletions tests/page-templates/05-angular/108-location-change-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*eslint-env mocha*/
describe("e2e/05-angular/108-location-change-only", function() {
var tf = BOOMR.plugins.TestFramework;
var t = BOOMR_test;

var pathName = window.location.pathname;

it("Should pass basic beacon validation", function(done) {
t.validateBeaconWasSent(done);
});

it("Should have sent three beacons", function() {
assert.equal(tf.beacons.length, 3);
});

it("Should have sent the first beacon as http.initiator = spa_hard", function() {
assert.equal(tf.beacons[0]["http.initiator"], "spa_hard");
});

it("Should have sent all subsequent beacons as http.initiator = spa", function() {
for (var i = 1; i < 2; i++) {
assert.equal(tf.beacons[i]["http.initiator"], "spa");
}
});

it("Should have sent all subsequent beacons have rt.nstart = navigationTiming (if NavigationTiming is supported)", function() {
if (typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
for (var i = 1; i < 2; i++) {
assert.equal(tf.beacons[i]["rt.nstart"], BOOMR.plugins.RT.navigationStart());
}
}
});

it("Should not have Boomerang timings on SPA Soft beacons", function() {
for (var i = 1; i < 2; i++) {
if (tf.beacons[i].t_other) {
assert.equal(tf.beacons[i].t_other.indexOf("boomr_fb"), -1, "should not have boomr_fb");
assert.equal(tf.beacons[i].t_other.indexOf("boomr_ld"), -1, "should not have boomr_ld");
assert.equal(tf.beacons[i].t_other.indexOf("boomr_lat"), -1, "should not have boomr_lat");
assert.equal(tf.beacons[i].t_other.indexOf("boomerang"), -1, "should not have boomerang");
}

// Boomerang and config timing parameters
assert.isUndefined(tf.beacons[i]["rt.bmr"]);
assert.isUndefined(tf.beacons[i]["rt.cnf"]);
}
});

//
// Beacon 1
//
it("Should have sent the first beacon for " + pathName, function() {
var b = tf.beacons[0];
assert.isTrue(b.u.indexOf(pathName) !== -1);
});

it("Should have a load time (if MutationObserver and NavigationTiming are supported)", function() {
if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
var b = tf.beacons[0];
assert.isDefined(b.t_done);
}
});

it("Should not have a load time (if MutationObserver is supported but NavigationTiming is not)", function() {
if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() === "undefined") {
var b = tf.beacons[0];
assert.equal(b.t_done, undefined);
}
});

it("Should take as long as the XHRs (if MutationObserver is not supported but NavigationTiming is)", function() {
if (typeof window.MutationObserver === "undefined" && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
t.validateBeaconWasSentAfter(0, "widgets.json", 500, 0, 30000, false);
}
});

it("Shouldn't have a load time (if MutationObserver and NavigationTiming are not supported)", function() {
if (typeof window.MutationObserver === "undefined" && typeof BOOMR.plugins.RT.navigationStart() === "undefined") {
var b = tf.beacons[0];
assert.equal(b.t_done, undefined);
assert.equal(b["rt.start"], "none");
}
});

it("Should have a t_resp of the root page (if MutationObserver and NavigationTiming are supported)", function() {
if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
var pt = window.performance.timing;
var b = tf.beacons[0];
assert.equal(b.t_resp, pt.responseStart - pt.fetchStart);
}
});

it("Should have a t_page of total - t_resp (if MutationObserver and NavigationTiming are supported)", function() {
if (window.MutationObserver && typeof BOOMR.plugins.RT.navigationStart() !== "undefined") {
var pt = window.performance.timing;
var b = tf.beacons[0];
assert.equal(b.t_page, b.t_done - b.t_resp);
}
});

//
// Beacon 2
//
it("Should have sent the second beacon for /nothing", function() {
var b = tf.beacons[1];
assert.isTrue(b.u.indexOf("/nothing") !== -1);
});

it("Should have sent the second beacon with a timestamp of ~0 seconds (if MutationObserver is supported)", function() {
if (window.MutationObserver) {
var b = tf.beacons[1];
assert.closeTo(b.t_done, 0, 50);
}
});

//
// Beacon 3
//
it("Should have sent the third beacon for " + pathName, function() {
var b = tf.beacons[2];
assert.isTrue(b.u.indexOf(pathName) !== -1);
});

it("Should have sent the third with a timestamp of at around 0 seconds (if MutationObserver is supported)", function() {
if (window.MutationObserver) {
var b = tf.beacons[2];
assert.closeTo(b.t_done, 0, 50);
}
});

it("Should have set the same Page ID (pid) on all beacons", function() {
var pid = tf.beacons[0].pid;
for (var i = 0; i <= 2; i++) {
var b = tf.beacons[i];
assert.equal(b.pid, pid);
}
});
});
Loading

0 comments on commit 031c1fb

Please sign in to comment.