Single Page Application Monitoring (Beta)

SPA Instrumentation

NOTE: SPA support is currently in beta

In order to monitor Single Page Applications, additional steps needs to be done. Depending on the frameworks used in SPA, and more specifically, depending on the router model used in the framework, you need to instrument the agent when the route transition starts, when it ends and when it fails.

Prerequisites

Before continuing, make sure that you already installed agent to your application. This will make ineum function globally available in your app.

Instana can segment website metrics by logical pages. To do so, it needs a hint what page the user is currently looking at. This page name can be set via the page command. We recommend to set the page as early as possible. Additionally, it is perfectly fine to change the page at some point to present document changes.

ineum('page', pageName: string);

Here is a minimal installation example:

<script>
  (function(i,s,o,g,r,a,m){i['InstanaEumObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//eum.instana.io/eum.min.js','ineum');

  ineum('apiKey', 'someKey');
  ineum('page', '/');
</script>

To get everything from our SPA monitoring solution, please make sure to read API documentation.

Angular 1.5 with ngRoute

function mainAppDirective($rootScope) {
  $rootScope.$on('$routeChangeStart', (event, next, current) => {
      if (next && next.$$route) {
        ineum('page', next.$$route.controller);
      }
      ineum('startSpaPageTransition');
  });

  $rootScope.$on('$routeChangeSuccess', (event, next, current) => {
    ineum('meta', 'controller', next && next.$$route ? next.$$route.controller : '');
    ineum('endSpaPageTransition', {
      status: 'completed',
      url: window.location.href
    });
  });

  $rootScope.$on('$routeChangeError', (event, next, current, rejection) => {
    ineum('endSpaPageTransition', {
      status: 'error',
      url: window.location.href,
      explanation: rejection.toString()
    });
  });
}

Angular 1.5 with ui-router

angular
  .module('common', ['ui.router'])
  .run(($transitions) => {
    $transitions.onStart({}, (trans) => {
      ineum('page', trans.to().name);
      ineum('startSpaPageTransition');
    });

    $transitions.onSuccess({}, (trans) => {
      ineum('meta', 'controller', trans.to().name);
      ineum('endSpaPageTransition', {
        status: 'completed',
        url: trans.router.stateService.href(trans.to().name, trans.params())
      });
    });

    $transitions.onError({}, (trans) => {
      ineum('endSpaPageTransition', {
        status: 'error',
        url: trans.router.stateService.href(trans.to().name, trans.params()),
        explanation: trans.error().toString()
      });
    });
  });

Angular 4/5

// inside main app component
ngOnInit() {
  this.router.events.subscribe(event => {
      var componentName = (this.activatedRoute.component !== null) ? this.activatedRoute.component['name'] : 'default';

    if (event instanceof NavigationStart) {
      ineum('page', componentName);
      ineum('startSpaPageTransition');
    } else if (event instanceof NavigationEnd) {
      ineum('meta', 'component', componentName);
      ineum('endSpaPageTransition', {status: 'completed'});
    } else if(event instanceof NavigationError) {
      ineum('endSpaPageTransition', {
        status: 'error',
        url: window.location.href,
        explanation: event.error.toString()
      });
    } else if(event instanceof NavigationCancel) {
      ineum('endSpaPageTransition', {
        status: 'aborted',
        url: window.location.href,
        explanation: event.reason
      });
    }
  });
}

React with react-router or react-router-redux

This approach is a more general one, meaning that it could be used with various routers.

First we define a WebsiteMonitoring component:

import React, { Component } from 'react';

class WebsiteMonitoring extends Component {
  componentWillMount() {
    ineum('page', '/' + window.location.pathname.split('/')[1]);
    ineum('startSpaPageTransition');
  }

  componentDidMount() {
    ineum('meta', 'content', '/' + window.location.pathname.split('/')[1]);
    ineum('endSpaPageTransition', {
      status: 'completed',
      url: window.location.href
    });   
  }

  componentWillUpdate() {
    ineum('page', '/' + window.location.pathname.split('/')[1]);
    ineum('startSpaPageTransition');
  }

  componentDidUpdate() {
    ineum('meta', 'content', '/' + window.location.pathname.split('/')[1]);
    ineum('endSpaPageTransition', {
      status: 'completed',
      url: window.location.href
    });
  }

  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

Then we add it on top of our main app component:

ReactDOM.render(
  <Router history={history}>
    <WebsiteMonitoring>
      <App>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
      </App>
    </WebsiteMonitoring>
  </Router>
, appRoot);

React with universal-router

We just need to modify the action function and instrument the tracker when the transition starts and when it ends:

// The top-level (parent) route
const routes = {
  path: '',

  children: [
    {
      path: '',
      load: () => import('./home'),
    }/*, ... */
  ],

  async action(context) {
    let route;

    ineum('page', context.pathname);
    ineum('startSpaPageTransition');

    try {
      route = await context.next();
      ineum('endSpaPageTransition', {status: 'completed'});
    } catch (error) {
      ineum('endSpaPageTransition', {
        status: 'error',
        url: window.location.href,
        explanation: error.toString()
      });
    }
    return route;
  }
};

Vue with default router

router.beforeEach((to, from, next) => {
  ineum('page', '/' + window.location.pathname.split('/')[1]);
  ineum('startSpaPageTransition');
  next();
});

router.afterEach((to, from) => {
  ineum('meta', 'content', to);
  ineum('endSpaPageTransition', {status: 'completed'});
});