AngularJS SPA Webpack Browser app with dynamic Application ID

AngularJS SPA Browser app bundled with Webpack

background

  • 1.6x AngularJS
  • Single Page App
  • Webpack Bundling
  • New Relic Browser Pro+SPA (copy/paste)
  • Dynamic New Relic application id
  • Custom page actions

I have two applications set up for the same project, one for QA and one for PROD. This allows me to set up two different new relic views. One to verify that events and page actions are being tracked as expected when our application is deployed to QA. And a completely separate view for monitoring production events & page actions.

error

I’m seeing the following console error:

Uncaught ReferenceError: NREUM is not defined
    at new-relic.agent.js:17

code snippets

src/index.html

<!doctype html>
<html ng-app="my-app">
  <head>
    <script type="text/javascript" src="js/vendor.js"></script>
    <script type="text/javascript" src="js/app.js"></script>
    <script type="text/javascript" src="js/newrelic.js"></script>
  </head>
  <body>
    <div id="container" ui-view></div>
  </body>
</html>

src/new-relic.agent.js

Browser Pro+SPA copy/paste code snippet

(function() {
  let newrelicIsConfigured = ENVIRONMENT_VARS
    && ENVIRONMENT_VARS.newrelic;
  if (newrelicIsConfigured) {
    window.NREUM||(NREUM={}),__nr_require=function(t,e,n){
      // intentionally omitted static section of snippet
      ...,licenseKey:ENVIRONMENT_VARS.newrelic.licenseKey,
      applicationID:ENVIRONMENT_VARS.newrelic.applicationID,
      sa:1}
  }
})();

webpack/qa.config.js

const webpack = require('webpack');
const environmentVars = require('./environment-vars/qa.vars');

let config = {
  entry: {
    app: `${paths.source}/app/root.module.js`,
    vendor: `${paths.source}/vendor/vendor.js`,
    newrelic: `${paths.source}/new-relic.agent.js`
  },
  output: {
    path: `${paths.dist}/`,
    filename: 'js/[name].js'
  },
  plugins: [
    new webpack.DefinePlugin({
      ENVIRONMENT_VARS: JSON.stringify(environmentVars)
    })
  ]
};
module.exports = config;

webpack/environment-vars/qa.vars.js

const envVars = {
  environment: 'qa',
  newrelic: {
    // note: actual values intentionally omitted
    licenseKey: '',
    applicationID: ''
  }
};
module.exports = envVars;
1 Like

@sbeagin,

AngularJS can definitely be a bit of a bear to instrument correctly, but let’s see what we can do to figure this out for you.

There are a couple of things that I see with the (very detailed, which we appreciate!) code samples you provided.

First, we technically don’t support the use of external scripts. If you’re using the copy and paste method to enable Browser, we require that it be placed inline near the top of the <HEAD> tag, after any position sensitive meta tags but before any other scripts are loaded. This ensures the instrumentation code is loaded before all other scripts so that wrapping will occur when other libraries are registered.

With that being said, I’ve seen other customers using external scripts like this to accommodate environmental variables, but we don’t recommend it because it can cause unexpected errors and/or lost reporting due to scripts being loaded at the wrong time.

Second, if the snippets you included in your post are accurate, your script file seems like it’s been named src/new-relic.agent.js but your link to it in the header is named src="js/newrelic.js". If your application is reporting at all, that’s probably not the issue, but I thought it was worth mentioning.

Do you happen to know what version of our loader agent are you using? (If you’re not sure, you can search for search for “js-agent.newrelic.com/nr-” in the copy and paste script.)

`

3 Likes

@hwilkalis thank you so much for your prompt reply!

I understand that you technically don’t support this, so I really do appreciate you taking the time to help me debug this error.

I always heard that best practices is to keep a separation of concerns within a code base. so we avoid inline styling defined in html, html templates defined in javascript files, and javascript defined in html files. I am surprised that there is a difference between inline javascript that that is defined inside <script> tags and javascript from a file that is loaded inside <script> tags inside the <HEAD> tags.

when my app is built via webpack (see webpack/qa.config.js above), it’s reading in src/new-relic.agent.js and will output a file newrelic.js in the /dist/js folder. I can confirm this is working as expected locally by running the build.

here is the loader version found in my copy/paste snippet: js-agent.newrelic.com/nr-spa-1071.min.js

thank you again so much for your help!!

@sbeagin,

You picked a good time to write in! Yesterday we were pretty busy, but today the ticket queue has been a little calmer so I’ve had some time to spend in the forum doing some research. :nerd_face:

I know that our inline placement is a little unusual, but per our product engineers, it’s the best way to capture everything we need to capture while also having only a tiny impact on app performance. When scripts are bundled or loaded externally, there’s no way that they’re guaranteed to load early enough.
At the very least I would advise moving the newrelic.js script up in the head so that it loads first.

So, now that I’ve issued the caveat that this is an unsupported use and I can’t guarantee correct reporting… That error message would indicate to me that it’s not acknowledging New Relic’s existence, so you may be missing a definition in your app.

I’ll keep digging around in our documentation around Angular apps - in the meantime would you be open to giving it a try using the inline script method we recommend to see if we can at least get it reporting for you?

2 Likes

@hwilkalis so sorry for the delayed response!

I can confirm that, when i try the inline script method, new relic works as expected & I see data in the QA browser app.

Once that was confirmed, fixed a typo I found in my new-relic.agent.js file and I moved the newrelic.js script up in the head so that it loads first, and I’m no longer seeing the error: NREUM is not defined.

However, it looks like it’s not even getting to the new relic snippet anymore. I added a debugger statement to the top of new-relic.agent.js and it’s no longer reached. I also am seeing that newrelic is not defined where I’m calling newrelic.addPageAction() in my app (this did not happen with the inline script method).

If I update my app to use the inline script method with hardcoded app & license IDs, how would you recommend viewing Browser data for QA separate from PROD? I know I could remove the domain conditions for my new relic browser app, and in Insights I can probably use the url to create separate views for QA and PROD. However, I don’t know how I would do this in the Browser Overview, Page Views, AJAX … etc. views.

@sbeagin,

And now I’m sorry to be the one to leave you hanging! It has been a busy week of many tickets.

Usually when we have customers who want to monitor a variety of environments we recommend using APM monitoring and rollup apps to modify where the app reports to based on environmental variables. You can find more information on that here: https://docs.newrelic.com/docs/agents/manage-apm-agents/app-naming/use-multiple-names-app. Otherwise I think you’d have to manually replace the application ID when moving from one environment to another.

I can’t tell which of the applications on your account you’re working with, but if you want to share a permalink to it here I’ll take a look and see if I can think of anything further for you.

1 Like

@hwilkalis haha no worries at all. I know how that goes :]

I was under the impression that APM is used for monitoring backend projects & that Browser/SPA is used for monitoring front end projects. But I’ll be the first to admit that my new relic knowledge and experience is very limited. Thank you for sharing the link to that documentation, I will take a look to see if that clears it up for me :smile:

in the mean time, here is the QA permalink & the Production permalink for the Browser app I am tracking (same project/code base).

thanks!!

1 Like

@sbeagin,

In training this week, but I just checked out both accounts and I see you have data reporting, so it seems like you’ve had some success with the inline script method - that’s a start!

You’re not incorrect - APM is intended more for backend monitoring. But if you’re able to instrument Browser with one of our APM agents you get the benefit of that backend information on your apps plus the additional custom configuration options available with the agents that allow you to change out the application IDs between environments without having to update scripts manually. Check out the documentation on that instrumentation method and let me know if you think this might be an option for you.

Cheers!

Holly

1 Like

@hwilkalis , it’s been a busy week on my end as well. yes, we were able to get a solution working; although it is probably still technically unsupported.

I am seeing page views & page actions coming from both QA & PROD separately.

Thank for sharing the documentation on the APM instrumentation method you described.
I’ll definitely walk through the process to add a new browser agent this way, following the directions in the doc, to see if this solution meets our needs.

just in case you’re curious, here’s a sanitized outline of the solution we came up with:

src/index.html

<!doctype html>
<html ng-app="my-app">
  <head>
    <script type="text/javascript">
      /* New Relic Pro+SPA Init Snippet */
      window.NREUM||(NREUM={})...;
      ; // moved last line, beginning with NREUM.info, to new-relic.module.js to support dynamic app IDs
    </script>
  
    <script type="text/javascript" src="js/vendor.js"></script>
    <script type="text/javascript" src="js/app.js"></script>
  </head>
  <body>
    <div id="container" ui-view></div>
  </body>
</html>

src/app/common/services/new-relic/new-relic.module.js

this file is imported in src/app/root.module.js. as defined in webpack/qa.config.js (below), when webpack bundles the app, the output for the root.module entry point is dist/js/app.js, which is included in the <head> of index.html.

import service from './new-relic.service';

export default angular.module('common.new-relic', [])
  .service('NewRelic', service)
  .run((myConfig, $window) => {
    'ngInject';

    /**
      * NewRelic's JavaScript agent
      * Usage:
      *   The agent will be setup ONLY if config.newrelic contains licenseKey and applicationID
      *
      * Updating the Agent:
      *   Copy Pro+SPA code snippet from NewRelic.
      *   Snippet contains two lines of minified code nested inside <script> tags.
      *   Paste first line (beginning with "window.NREUM") inside <script> tags in index.html.
      *   Paste second line (beginning with ";NREUM.info") inside if block below.
      *   Make the following updates to the code snippet pasted below:
      *   - Update to access NREUM object via $window.
      *   - Update the licenseKey and applicationID (use config variables)
      */
    let configured = myConfig
      && myConfig.newrelic
      && myConfig.newrelic.licenseKey
      && myConfig.newrelic.applicationID;
    if (configured) {
      // if we have configured newrelic for this environment, load newrelic js agent
      $window.NREUM.info = {
        beacon: 'bam.nr-data.net',
        errorBeacon: 'bam.nr-data.net',
        licenseKey: myConfig.newrelic.licenseKey,
        applicationID: myConfig.newrelic.applicationID,
        sa: 1
      };
    }
  })
  .name;

src/app/common/services/new-relic/new-relic.service.js

const me = 'NewRelicService';
class NewRelicService {
  constructor(Logger, $window) {
    'ngInject';
    this.logger = Logger;
    this.window = $window;
  }

  addPageAction(action, data) {
    let logData = {
      action: action,
      data: data
    };
    this.logger.info('addPageAction', logData, me);
    if (angular.isDefined(this.window)
      && angular.isDefined(this.window.newrelic)) {
      this.window.newrelic.addPageAction(action, data);
    }
    else {
      this.logger.warn('newrelic is not available', {}, me);
    }
  }
}

export default NewRelicService;

src/app/common/services/config.module.js

// Defines Webpack globals into an Angular Service.
class service {
  constructor() {
    this.environment = ENVIRONMENT_VARS.environment;
    this.newrelic = ENVIRONMENT_VARS.newrelic;
    this.logLevel = ENVIRONMENT_VARS.logLevel;
    this.featureFlags = ENVIRONMENT_VARS.featureFlags;
  }
}

export default angular
  .module('common.services.config', [])
  .service('myConfig', service)
  .name;

webpack/qa.config.js

const webpack = require('webpack');
const paths = require('./support/paths');
const environmentVars = require('./environment-vars/qa.vars');

let config = {
  entry: {
    app: `${paths.source}/app/root.module.js`,
    vendor: `${paths.source}/vendor/vendor.js`
  },
  output: {
    path: `${paths.dist}/`,
    filename: 'js/[name].js'
  },
  plugins: [
    new webpack.DefinePlugin({
      ENVIRONMENT_VARS: JSON.stringify(environmentVars)
    })
  ]
};
module.exports = config;

webpack/environment-vars/qa.vars.js

const envVars = {
  environment: 'qa',
  newrelic: {
    // note: actual values intentionally omitted
    licenseKey: '',
    applicationID: ''
  }
};
module.exports = envVars;

Thanks again for all your help!! :smiley:

9 Likes

@sbeagin,

Thanks for the update and for sharing your creative solution to this challenge! This comes up pretty regularly in support cases, so I think I might share this post with some teammates to get their take on it too.

Cheers!

-Holly

3 Likes