From b9900c9735c42f836756a77cf68765ab3d36c99e Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Fri, 9 Dec 2016 19:54:16 -0500 Subject: [PATCH 1/8] new app insights provider --- package.json | 2 +- .../angulartics2-appinsights.spec.ts | 160 ++++++++++++++++++ .../appinsights/angulartics2-appinsights.ts | 146 ++++++++++++++++ src/providers/index.ts | 1 + tsconfig.json | 3 +- 5 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 src/providers/appinsights/angulartics2-appinsights.spec.ts create mode 100644 src/providers/appinsights/angulartics2-appinsights.ts diff --git a/package.json b/package.json index 930701d4..1baaf5b8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "Fil Maj (https://github.com/filmaj)", "Adam Yost (https://github.com/swimmadude66)" ], - "dependencies": {}, "devDependencies": { "@angular/common": "^2.1.2", "@angular/compiler": "^2.1.2", @@ -43,6 +42,7 @@ "@angular/platform-browser": "^2.1.2", "@angular/platform-browser-dynamic": "^2.1.2", "@angular/router": "^3.0.0", + "@types/applicationinsights-js": "^0.23.5", "@types/core-js": "^0.9.32", "@types/jasmine": "^2.2.29", "@types/node": "6.0.38", diff --git a/src/providers/appinsights/angulartics2-appinsights.spec.ts b/src/providers/appinsights/angulartics2-appinsights.spec.ts new file mode 100644 index 00000000..ce62d58b --- /dev/null +++ b/src/providers/appinsights/angulartics2-appinsights.spec.ts @@ -0,0 +1,160 @@ +import { Location } from '@angular/common'; +import { SpyLocation } from '@angular/common/testing'; +import { TestBed, ComponentFixture, fakeAsync, inject } from '@angular/core/testing'; + +import { TestModule, RootCmp, advance, createRoot } from '../../test.mocks'; + +import { Angulartics2 } from '../../core/angulartics2'; +import { Angulartics2AppInsights } from './angulartics2-appinsights'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Title } from '@angular/platform-browser'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; +declare let window: any; + +describe('Angulartics2AppInsights', () => { + let appInsights: Microsoft.ApplicationInsights.IAppInsights; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TestModule, + RouterTestingModule, + ], + providers: [ + { provide: Location, useClass: SpyLocation }, + Title, + Angulartics2AppInsights + ] + }); + + window.appInsights = appInsights = jasmine.createSpyObj( + 'appInsights', + [ + 'trackPageView', + 'trackEvent', + 'trackException', + 'setAuthenticatedUserContext' + ] + ); + }); + + it('should track pages', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights, Title], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights, title: Title) => { + fixture = createRoot(RootCmp); + let metrics = {}; + let dimensions = {}; + let loadTime = 123; + spyOn(title, 'getTitle').and.returnValue('the title'); + angulartics2AppInsights.metrics = metrics; + angulartics2AppInsights.dimensions = dimensions; + angulartics2AppInsights.loadTime = loadTime; + angulartics2.pageTrack.next({ path: '/abc', location: location }); + advance(fixture); + expect(appInsights.trackPageView).toHaveBeenCalledWith('the title', '/abc', metrics, dimensions, loadTime); + })) + ); + + it('should track events', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let action = 'the action'; + let properties = {}; + let measurements = {}; + angulartics2AppInsights.measurements = measurements; + angulartics2.eventTrack.next({ + action, properties + }); + advance(fixture); + expect(appInsights.trackEvent).toHaveBeenCalledWith(action, properties, measurements); + }))); + + it('should track exceptions (string)', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let str = 'test string'; + angulartics2.exceptionTrack.next(str); + advance(fixture); + expect(appInsights.trackException).toHaveBeenCalledWith(str); + }))); + + it('should track exceptions (event)', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let event = { 'event': true }; + angulartics2.exceptionTrack.next({ event }); + advance(fixture); + expect(appInsights.trackException).toHaveBeenCalledWith(event); + }))); + + it('should track exceptions (description)', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let description = 'test description'; + angulartics2.exceptionTrack.next({ description }); + advance(fixture); + expect(appInsights.trackException).toHaveBeenCalledWith(description); + }))); + + it('should set userId in setAuthenticatedUserContext', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let userId = 'test_userId'; + angulartics2AppInsights.setUsername(userId); + advance(fixture); + expect(angulartics2.settings.appInsights.userId).toBe(userId); + expect(appInsights.setAuthenticatedUserContext).toHaveBeenCalledWith(userId); + }))); + + + it('should set userId and accountId in setAuthenticatedUserContext', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let userId = 'test_userId'; + let accountId = 'test_accountId'; + angulartics2AppInsights.setUserProperties({ userId, accountId }); + advance(fixture); + expect(angulartics2.settings.appInsights.userId).toBe(userId); + expect(appInsights.setAuthenticatedUserContext).toHaveBeenCalledWith(userId, accountId); + }))); + + + it('should user existing userId and set accountId in setAuthenticatedUserContext', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + fixture = createRoot(RootCmp); + let userId = 'test_userId'; + let accountId = 'test_accountId'; + angulartics2AppInsights.setUsername(userId); + advance(fixture); + expect(angulartics2.settings.appInsights.userId).toBe(userId); + angulartics2AppInsights.setUserProperties({ accountId }); + advance(fixture); + expect(appInsights.setAuthenticatedUserContext).toHaveBeenCalledWith(userId, accountId); + }))); + + it('should set the start time on start', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + angulartics2AppInsights.startTimer(); + expect(angulartics2AppInsights.loadStartTime).toBe(Date.now()); + expect(angulartics2AppInsights.loadTime).toBe(null); + }))); + + it('should set the total time on stop', + fakeAsync(inject([Location, Angulartics2, Angulartics2AppInsights], + (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { + angulartics2AppInsights.loadStartTime = Date.now() - 1000; + angulartics2AppInsights.stopTimer(); + expect(angulartics2AppInsights.loadTime).toBe(1000); + expect(angulartics2AppInsights.loadStartTime).toBe(null); + }))); +}); \ No newline at end of file diff --git a/src/providers/appinsights/angulartics2-appinsights.ts b/src/providers/appinsights/angulartics2-appinsights.ts new file mode 100644 index 00000000..355ef707 --- /dev/null +++ b/src/providers/appinsights/angulartics2-appinsights.ts @@ -0,0 +1,146 @@ +import { Injectable, OnDestroy, OnInit } from '@angular/core'; + +import { Angulartics2 } from '../../core/angulartics2'; + +declare const appInsights: Microsoft.ApplicationInsights.IAppInsights; + +import { Title } from '@angular/platform-browser'; +import { Router, NavigationEnd, NavigationStart, NavigationError } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Injectable() +export class Angulartics2AppInsights implements OnInit, OnDestroy { + loadStartTime: number = null; + loadTime: number = null; + private sub: Subscription[]; + + metrics: any = null; + dimensions: any = null; + measurements: any = null; + + constructor(private angulartics2: Angulartics2, + private title: Title, + private router: Router) { + if (typeof (appInsights) === 'undefined') { + console.warn('appInsights not found'); + } + + this.angulartics2.settings.appInsights = { + userId: null + }; + + this.angulartics2.pageTrack.subscribe((x: any) => this.pageTrack(x.path)); + + this.angulartics2.eventTrack.subscribe((x: any) => this.eventTrack(x.action, x.properties)); + + this.angulartics2.exceptionTrack.subscribe((x: any) => this.exceptionTrack(x)); + + this.angulartics2.setUsername.subscribe((x: string) => this.setUsername(x)); + + this.angulartics2.setUserProperties.subscribe((x: any) => this.setUserProperties(x)); + } + + ngOnInit(): void { + this.sub.push( + this.router.events + .filter(event => event instanceof NavigationStart) + .subscribe(event => this.startTimer()) + ); + + this.sub.push( + this.router.events + .filter(event => + (event instanceof NavigationError) || + (event instanceof NavigationEnd) + ) + .subscribe(error => this.stopTimer()) + ); + } + + startTimer() { + this.loadStartTime = Date.now(); + this.loadTime = null; + } + + stopTimer() { + this.loadTime = Date.now() - this.loadStartTime; + this.loadStartTime = null; + } + + ngOnDestroy(): void { + this.sub.forEach(sub => sub.unsubscribe()); + + this.sub = []; + } + + /** + * Page Track in Baidu Analytics + * @name pageTrack + * + * @param {string} path Required 'path' (string) + * + * @link https://github.com/Microsoft/ApplicationInsights-JS/blob/master/API-reference.md#trackpageview + * + */ + pageTrack(path: string) { + appInsights.trackPageView( + this.title.getTitle(), + path, + this.dimensions, + this.metrics, + this.loadTime + ); + } + + /** + * Log a user action or other occurrence. + * @param name A string to identify this event in the portal. + * + * @param properties map[string, string] - additional data used to filter events and metrics in the portal. Defaults to empty. + * + * @https://github.com/Microsoft/ApplicationInsights-JS/blob/master/API-reference.md#trackevent + */ + eventTrack(name: string, properties: any) { + appInsights.trackEvent(name, properties, this.measurements); + } + + /** + * Exception Track Event in GA + * @name exceptionTrack + * + * @param {any} properties Comprised of the mandatory fields 'appId' (string), 'appName' (string) and 'appVersion' (string) and + * optional fields 'fatal' (boolean) and 'description' (string), error + * + * @link https://github.com/Microsoft/ApplicationInsights-JS/blob/master/API-reference.md#trackexception + */ + exceptionTrack(properties: any) { + let description = properties.event || properties.description || properties; + + appInsights.trackException(description); + } + + /** + * + * @param userId + * + * @link https://github.com/Microsoft/ApplicationInsights-JS/blob/master/API-reference.md#setauthenticatedusercontext + */ + + setUsername(userId: string) { + this.angulartics2.settings.appInsights.userId = userId; + appInsights.setAuthenticatedUserContext(userId); + } + + setUserProperties(properties: any) { + if (properties.userId) { + this.angulartics2.settings.appInsights.userId = properties.userId; + } + + if (properties.accountId) { + appInsights.setAuthenticatedUserContext( + this.angulartics2.settings.appInsights.userId, + properties.accountId + ); + } + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index fa822e72..ce050a19 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -6,3 +6,4 @@ export * from './mixpanel/angulartics2-mixpanel'; export * from './piwik/angulartics2-piwik'; export * from './segment/angulartics2-segment'; export * from './facebook/angulartics2-facebook'; +export * from './appinsights/angulartics2-appinsights'; diff --git a/tsconfig.json b/tsconfig.json index f631fe29..73e0b5d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "moduleResolution": "node", "types": [ "jasmine", - "node" + "node", + "applicationinsights-js" ], "lib": ["es2015", "dom"], "outDir": "dist" From 8a4dd2917a85bc09376435a33c751e570546512a Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Fri, 9 Dec 2016 20:21:00 -0500 Subject: [PATCH 2/8] adding a version --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1baaf5b8..a7dfd5fa 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "angulartics2", + "version": "0.0.0", "description": "Vendor-agnostic web analytics for Angular2 applications", "homepage": "http://angulartics.github.io/", "author": "João Ribeiro (http://github.com/JonnyBGod)", From 1464fe3ae203436bddc0b20b6be7999fd4434365 Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Fri, 9 Dec 2016 20:48:51 -0500 Subject: [PATCH 3/8] removing version --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index a7dfd5fa..1baaf5b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "angulartics2", - "version": "0.0.0", "description": "Vendor-agnostic web analytics for Angular2 applications", "homepage": "http://angulartics.github.io/", "author": "João Ribeiro (http://github.com/JonnyBGod)", From f9d31398b25acd7f291297125c78690f46b1b21e Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Mon, 12 Dec 2016 10:44:20 -0500 Subject: [PATCH 4/8] updating comment --- src/providers/appinsights/angulartics2-appinsights.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/appinsights/angulartics2-appinsights.ts b/src/providers/appinsights/angulartics2-appinsights.ts index 355ef707..416df7f3 100644 --- a/src/providers/appinsights/angulartics2-appinsights.ts +++ b/src/providers/appinsights/angulartics2-appinsights.ts @@ -74,7 +74,7 @@ export class Angulartics2AppInsights implements OnInit, OnDestroy { } /** - * Page Track in Baidu Analytics + * Page Track in App Insights * @name pageTrack * * @param {string} path Required 'path' (string) From 156d7864530d8c2e3909eea05e06146b358624ce Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Mon, 12 Dec 2016 12:17:33 -0500 Subject: [PATCH 5/8] @Injectables() don't have OnInit or OnDestory, removing --- .../angulartics2-appinsights.spec.ts | 4 +- .../appinsights/angulartics2-appinsights.ts | 38 ++++++------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/src/providers/appinsights/angulartics2-appinsights.spec.ts b/src/providers/appinsights/angulartics2-appinsights.spec.ts index ce62d58b..75729e2b 100644 --- a/src/providers/appinsights/angulartics2-appinsights.spec.ts +++ b/src/providers/appinsights/angulartics2-appinsights.spec.ts @@ -154,7 +154,9 @@ describe('Angulartics2AppInsights', () => { (location: Location, angulartics2: Angulartics2, angulartics2AppInsights: Angulartics2AppInsights) => { angulartics2AppInsights.loadStartTime = Date.now() - 1000; angulartics2AppInsights.stopTimer(); - expect(angulartics2AppInsights.loadTime).toBe(1000); + // 50ms time difference for testing to ensure timing is correct + expect(angulartics2AppInsights.loadTime).toBeLessThanOrEqual(1150); + expect(angulartics2AppInsights.loadTime).toBeGreaterThanOrEqual(1000); expect(angulartics2AppInsights.loadStartTime).toBe(null); }))); }); \ No newline at end of file diff --git a/src/providers/appinsights/angulartics2-appinsights.ts b/src/providers/appinsights/angulartics2-appinsights.ts index 416df7f3..963c2af0 100644 --- a/src/providers/appinsights/angulartics2-appinsights.ts +++ b/src/providers/appinsights/angulartics2-appinsights.ts @@ -1,4 +1,4 @@ -import { Injectable, OnDestroy, OnInit } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Angulartics2 } from '../../core/angulartics2'; @@ -6,13 +6,11 @@ declare const appInsights: Microsoft.ApplicationInsights.IAppInsights; import { Title } from '@angular/platform-browser'; import { Router, NavigationEnd, NavigationStart, NavigationError } from '@angular/router'; -import { Subscription } from 'rxjs'; @Injectable() -export class Angulartics2AppInsights implements OnInit, OnDestroy { +export class Angulartics2AppInsights { loadStartTime: number = null; loadTime: number = null; - private sub: Subscription[]; metrics: any = null; dimensions: any = null; @@ -38,23 +36,17 @@ export class Angulartics2AppInsights implements OnInit, OnDestroy { this.angulartics2.setUsername.subscribe((x: string) => this.setUsername(x)); this.angulartics2.setUserProperties.subscribe((x: any) => this.setUserProperties(x)); - } - ngOnInit(): void { - this.sub.push( - this.router.events - .filter(event => event instanceof NavigationStart) - .subscribe(event => this.startTimer()) - ); + this.router.events + .filter(event => event instanceof NavigationStart) + .subscribe(event => this.startTimer()); - this.sub.push( - this.router.events - .filter(event => - (event instanceof NavigationError) || - (event instanceof NavigationEnd) - ) - .subscribe(error => this.stopTimer()) - ); + this.router.events + .filter(event => + (event instanceof NavigationError) || + (event instanceof NavigationEnd) + ) + .subscribe(error => this.stopTimer()); } startTimer() { @@ -67,14 +59,8 @@ export class Angulartics2AppInsights implements OnInit, OnDestroy { this.loadStartTime = null; } - ngOnDestroy(): void { - this.sub.forEach(sub => sub.unsubscribe()); - - this.sub = []; - } - /** - * Page Track in App Insights + * Page Track in Baidu Analytics * @name pageTrack * * @param {string} path Required 'path' (string) From 2b648b03a4e1c82379e1963dc229f5bee283163b Mon Sep 17 00:00:00 2001 From: "Adam S. Kirschner" Date: Mon, 12 Dec 2016 12:33:20 -0500 Subject: [PATCH 6/8] adding self to contributors --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1baaf5b8..8dffbbba 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "Michael Cameron Delaney (https://github.com/NewMediaRoc)", "Chris Soyars (https://github.com/ctso)", "Fil Maj (https://github.com/filmaj)", - "Adam Yost (https://github.com/swimmadude66)" + "Adam Yost (https://github.com/swimmadude66)", + "Adam S. Kirschner (https://github.com/hikirsch)" ], "devDependencies": { "@angular/common": "^2.1.2", From 1b22ed208e52212150ae1f1fc332610356ff5a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ribeiro?= Date: Wed, 14 Dec 2016 05:37:41 +0000 Subject: [PATCH 7/8] revert back missing @types --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index dc96e37b..62347d8f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@angular/platform-browser": "~2.2.0", "@angular/platform-browser-dynamic": "~2.2.0", "@angular/router": "~3.2.0", + "@types/applicationinsights-js": "^0.23.5", "@types/core-js": "^0.9.32", "@types/jasmine": "^2.2.29", "@types/node": "6.0.38", From 31a22a26563248181eea824a7fd7ad780b6f5d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ribeiro?= Date: Wed, 14 Dec 2016 05:41:52 +0000 Subject: [PATCH 8/8] add application insights provider to read --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d5c8bcbd..9ee4eb34 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ this.angulartics2.eventTrack.next({ action: 'myAction', properties: { category: * Segment * Baidu Analytics * Facebook Pixel +* Application Insights ### For other providers