angularjsangularjs-serviceamplitude-analytics

AngularJS Service not acting as Singleton


Apologies for the code heavy post but I wanted to provide as much context as possible. I am having an issue with defining a service in my Angular.js application. Services are supposed to act as singletons throughout an application (source), so I am very confused to be getting the following behavior.

In my app.js file, I run my AmplitudeService Service and console.log(AmplitudeService). This outputs an object with all of the methods that I have defined within my AmplitudeService.js file. As such, I am able to properly use the service and log events as expected.

However, when I console.log(AmplitudeService) within my header.js, it outputs my Window object. As such, the Window does not contain functions such as "logEvent", "identifyUser", etc., so AmplitudeService is not usable in that case.

Would appreciate any and all insight!

AmplitudeService.js (source)

Note: If you check the author's syntax, he returns an object at the end of his service. In my research, I've read to use the "this" keyword when defining Service functions (source), and that you don't need to return an object as you would with a Factory, so I have updated it accordingly.

angular.module('AmplitudeService', [])
.service('AmplitudeService', 
['$amplitude', '$rootScope', 'amplitudeApiKey', '$location',
 function ($amplitude, $rootScope, amplitudeApiKey, $location) {

  this.init = function() {
    $amplitude.init(amplitudeApiKey, null);
    $amplitude.logEvent('LAUNCHED_SITE', {page: $location.$$path});
  }

  this.identifyUser = function(userId, userProperties) {
    $amplitude.setUserId(userId);
    $amplitude.setUserProperties(userProperties);
  }

  this.logEvent = function(eventName, params) {
    $amplitude.logEvent(eventName, params);
  }
}]);

angular-amplitude.js (source)

This allows access to "$amplitude" throughout the application

(function(){
var module = angular.module('angular-amplitude', ['ng']);

module.provider('$amplitude', [function $amplitudeProvider() {
    this.$get = ['$window', function($window) {
      (function(e,t){
        var r = e.amplitude || {};
        var n = t.createElement("script");
        n.type = "text/javascript";
      n.async = true;
      n.src = "https://d24n15hnbwhuhn.buttfront.net/libs/amplitude-2.2.0-min.gz.js";
      var s = t.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(n,s);
      r._q = [];

      function a(e){
        r[e] = function(){
          r._q.push([e].concat(Array.prototype.slice.call(arguments,0)));
        }
      }
      var i = ["init","logEvent","logRevenue","setUserId","setUserProperties","setOptOut","setVersionName","setDomain","setDeviceId","setGlobalUserProperties"];
      for(var o = 0; o < i.length; o++){
        a(i[o])
      }
      e.amplitude = r
    }
      )(window,document);
      return $window.amplitude;
    }];
}]);
return module;
}());

App.js

angular.module('app', [
'ngRoute',
'angular-amplitude',
'AmplitudeService',
])

 .run(['AmplitudeService', function(AmplitudeService){
 console.log(AmplitudeService); // Outputs 'Object {}'
 AmplitudeService.init();
 AmplitudeService.logEvent('LAUNCHED_SITE');
 console.log(AmplitudeService); // Outputs 'Object {}'
 }])

Header.js

angular.module('app.common.header', [])
.controller('HeaderCtrl', [ '$rootScope', '$scope', '$location', '$scope', '$route', '$window', 'AmplitudeService', function($rootScope, $scope, $location, $route, $window, AmplitudeService){

$scope.goToSearch = function(term) {
$location.path('/search/' + term);
 console.log(AmplitudeService); // Outputs 'Window {}'
};
}]);

Solution

  • Looks like you are injecting $scope in your controller, causing the unexpected mapping of the variable

    angular.module('app.common.header', [])
    .controller('HeaderCtrl', [ '$rootScope', '$scope', '$location', **'$scope'**, '$route', '$window', 'AmplitudeService', function($rootScope, $scope, $location, $route, $window, AmplitudeService){
    
    $scope.goToSearch = function(term) {
    $location.path('/search/' + term);
     console.log(AmplitudeService); // Outputs 'Window {}'
    };
    }]);
    

    as an aside if find this format far easier to read / work with when defining controllers

    angular
      .module('app.common.header', [])
      .controller('HeaderCtrl', headerController);
    
    headerController.$inject = [ '$rootScope', '$scope', '$location', '$route', '$window', 'AmplitudeService']
    
    function headerController($rootScope, $scope, $location, $route, $window, AmplitudeService){
      $scope.goToSearch = function(term) {
        $location.path('/search/' + term);
        console.log(AmplitudeService); // Outputs 'Window {}'
      };
    }