Skip to content
6 changes: 6 additions & 0 deletions generators/app/templates/sources/main/_main.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface IApplicationConfig {

export interface IApplicationEnvironment {
debug: boolean;
googleAnayticsId: string;
server: IServerConfig;
}

Expand All @@ -19,6 +20,10 @@ let environment = {
local: {
debug: true,

// Google Analytics account. Leave null to not have any analytics active.
// Typical values are of the form 'UA-########-1', where each # is a digit.
googleAnayticsId: null,

// REST backend configuration, used for all web services using restService
server: {
url: '',
Expand All @@ -27,6 +32,7 @@ let environment = {
},
production: {
debug: false,
googleAnayticsId: null,
server: {
<% if (props.target === 'web') { -%>
url: '',
Expand Down
20 changes: 19 additions & 1 deletion generators/app/templates/sources/main/_main.run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import app from 'main.module';
import {IApplicationConfig} from 'main.constants';
import {RestService} from 'helpers/rest/rest.service';
import {AnalyticsService} from 'helpers/analytics/analytics.service';
<% if (props.target !== 'web') { -%>
import {ILogger, LoggerService} from 'helpers/logger/logger';
<% } -%>
Expand All @@ -11,6 +12,7 @@ import {ILogger, LoggerService} from 'helpers/logger/logger';
*/
function main($window: ng.IWindowService,
$locale: ng.ILocaleService,
$location: ng.ILocationService,
$rootScope: any,
$state: angular.ui.IStateService,
<% if (props.target !== 'web') { -%>
Expand All @@ -26,7 +28,8 @@ function main($window: ng.IWindowService,
<% if (props.target !== 'web') { -%>
logger: LoggerService,
<% } -%>
restService: RestService) {
restService: RestService,
analyticsService: AnalyticsService) {

/*
* Root view model
Expand Down Expand Up @@ -84,6 +87,21 @@ function main($window: ng.IWindowService,
updateTitle($state.current.data ? $state.current.data.title : null);
});

/**
* Enables tracking by analytics service.
*/
// HACK : ignore the first $viewContentLoaded event because it's actually fired once when uiView is instantiated,
// and then it's fired a second time after is has been linked. This is "by design" :-/
// (http://stackoverflow.com/questions/31000417/angular-js-viewcontentloaded-loading-twice-on-initial-homepage-load)
let loadedOnce = false;
vm.$on('$viewContentLoaded', function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use $stateChangeSuccess like the hook to update title on route changes?
This should fix the need for the "hack" :)

Copy link
Author

@bursauxa bursauxa Jan 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per ui-router documentation (link) :

State change events are deprecated, DISABLED and replaced by Transition Hooks as of version 1.0

Note that the view load events are not deprecated. So maybe the real solution is to look into transition hooks ? But that's a different beast altogether.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL, missed that since we are still on the 0.3.x version and the 1.x version has not been finalized yet 😅

I have created a separate issue for the migration to using $transitions hooks (see #38 ), for the meanwhile it's still a valid change (until 1.0.0 version is out) that will get rid of the ugly hack, and it will simplify the migration task, as it will be needed anyway as we use it elsewhere.
Thanks for pointing that!

if (!loadedOnce) {
loadedOnce = true;
} else if (analyticsService) {
analyticsService.trackPage($location.url());
}
});

init();

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import app from 'main.module';
import {ILogger, LoggerService} from 'helpers/logger/logger';
import {IApplicationEnvironment} from 'main.constants';

const analyticsScriptUrl = '//www.google-analytics.com/analytics.js';

interface IWindowWithAnalytics extends ng.IWindowService {
ga: any;
}

/**
* Analytics service: insert Google Analytics library in the page.
*/
export class AnalyticsService {

private logger: ILogger;
private analyticsAreActive = false;

constructor(private $window: IWindowWithAnalytics,
private config: IApplicationEnvironment,
logger: LoggerService) {

this.logger = logger.getLogger('analyticsService');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger not needed if it's not used, or maybe it could be used to log tracked events?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, logging tracked events was the point. I must have lost it somewhere along the road ; will fix it now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done earlier (with previous changes).


this.init();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer a public init() method not called in constructor but in app init (in main.run.ts) explicitely, so we can also pass custom GA configuration as an optional argument for the create call.

This is also mandatory for mobile/Cordova compatibility, as specific configuration is needed for GA to work with file:// protocol used in Cordova.

This would also allow us to disable analytics in debug environment, as it is most of the time only wanted on production and it can mess with unit tests by generating extra HTTP calls.

See this example except from a mobile of mine, to make it work with Cordova (located in init() method of main.run.ts, it uses amgulartics but the idea is the same:

      // Setup analytics
      if (!config.environment.debug && $window['ga']) {
        // Warning, breaks unit tests if included in debug!
        $window['ga']('create', config.googleAnalyticsId, {
          'storage': 'none',
          'clientId': $cordovaDevice.getUUID()
        });
        // Allow file:// protocol for cordova
        $window['ga']('set', 'checkProtocolTask', (data: any) => {
          data.set('location', 'https://htf2016.app/');
        });
        $window['ga']('set', 'appVersion', config.version);
        $analytics.eventTrack('App started', { value: vm.festival.version });
      }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the responsibility of customizing the creation of the GA object lies within the analytics service. All the Cordova specific code can be added in analytics-service instead of main-run.

Regarding disabling of analytics in debug, there are some cases where having them on is useful (typically when one wants to test out a newly-added event tracker). Of course you would want a different analytics account for debugging and for actual production, but that is just a matter of setting up the gulp-replace task around IApplicationConfig.googleAnalyticsId. If you want to disable them entirely in debug mode, it can also be done the same way, by setting the value to null in debug mode.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the responsibility of customizing the creation of the GA object lies within the analytics service. All the Cordova specific code can be added in analytics-service instead of main-run.

I completely agree with that, I just posted it to show a mobile setup ;)

Regarding disabling of analytics in debug, there are some cases where having them on is useful (typically when one wants to test out a newly-added event tracker). Of course you would want a different analytics account for debugging and for actual production, but that is just a matter of setting up the gulp-replace task around IApplicationConfig.googleAnalyticsId. If you want to disable them entirely in debug mode, it can also be done the same way, by setting the value to null in debug mode.

In my idea disabling analytics in debug would be done in main.run.ts by calling or not the analyticsService.init(), so if needed the behavior can be changed. But it's a good idea to just check if googleAnalyticsId is defined to activate or not GA in the init() method, so the you just need to move the googleAnalyticsId in the environment object so there can be an value for each environment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: googleAnalyticsId moved from Config to Environment.

}

/**
* Tracks a page change in google analytics.
* @param {String} url The url of the new page.
*/
trackPage (url: string) {
if (this.analyticsAreActive) {
let urlWithoutParams = url;
let split = url.split('?');
if (split.length > 1) {
urlWithoutParams = split[0];
}
this.$window.ga('send', 'pageview', urlWithoutParams);
}
}

/**
* Sends a track event to google analytics.
* @param {String} category The category to be sent.
* @param {String} action The action to be sent.
* @param {String=} label The label to be sent.
*/
trackEvent (category: string, action: string, label?: string) {
if (this.analyticsAreActive) {
this.$window.ga('send', 'event', category, action, label);
let logMessage = 'Event tracked: ' + category + ' | ' + action;
if (label) {
logMessage += ' | ' + label;
}
this.logger.log(logMessage);
}
}

private init(): void {
if (this.config.googleAnayticsId !== null) {
this.createGoogleAnalyticsObject(this.$window, document, 'script', analyticsScriptUrl, 'ga');
this.$window.ga('create', this.config.googleAnayticsId, 'auto');
this.analyticsAreActive = true;
}
}

private createGoogleAnalyticsObject(i: any, s: any, o: any, g: any, r: any, a?: any, m?: any) {
i.GoogleAnalyticsObject = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
}
}

app.service('analyticsService', AnalyticsService);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it is a service, unit tests are mandatory (though they will be quite simple as GA will be mocked) and should be added :)