From 193c1c869b09a03a61950596a8d49239b3e0d4cc Mon Sep 17 00:00:00 2001 From: Aashil Date: Tue, 13 Sep 2016 17:36:23 -0400 Subject: [PATCH 1/3] PROD-276: Record custom events like clicking login, logout, show more button, submit button and pressing enter for searching and send it to GoogleAnalytics. * Create a separate GoogleAnalyticsService describing the methods to use for recording events to Google Analytics. --- default/src/app/app.module.ts | 3 ++- default/src/app/auth/auth-button.component.ts | 6 ++++-- default/src/app/auth/auth.service.ts | 11 ++++------- default/src/app/report/report.component.ts | 4 +++- default/src/app/search/search.component.ts | 7 ++++++- .../src/app/shared/google.analytics.service.ts | 15 +++++++++++++++ 6 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 default/src/app/shared/google.analytics.service.ts diff --git a/default/src/app/app.module.ts b/default/src/app/app.module.ts index 396183c..b89a538 100644 --- a/default/src/app/app.module.ts +++ b/default/src/app/app.module.ts @@ -9,7 +9,7 @@ import { AUTH_PROVIDERS } from 'angular2-jwt'; import { FocusMeDirective } from './shared/focus-me.directive'; import { BackendService } from './shared/backend.service'; import { KeysPipe } from './shared/keys.pipe'; - +import {GoogleAnalyticsService} from './shared/google.analytics.service'; import { AppComponent } from './app.component'; import { ReportComponent } from './report/index'; @@ -42,6 +42,7 @@ let providers = [ HTTP_PROVIDERS, AuthService, BackendService, + GoogleAnalyticsService, ]; let declarations = [ diff --git a/default/src/app/auth/auth-button.component.ts b/default/src/app/auth/auth-button.component.ts index 98709b5..fcc293f 100644 --- a/default/src/app/auth/auth-button.component.ts +++ b/default/src/app/auth/auth-button.component.ts @@ -3,7 +3,7 @@ */ import { Component } from '@angular/core'; import { AuthService } from './auth.service'; - +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; @Component({ selector: 'auth-button', @@ -13,7 +13,7 @@ import { AuthService } from './auth.service'; export class AuthButtonComponent { public loggedIn = false; - constructor(public auth: AuthService) { + constructor(public auth: AuthService, private ga: GoogleAnalyticsService) { this.auth.getUser().subscribe( user => { if (user && user.loggedIn) { @@ -25,9 +25,11 @@ export class AuthButtonComponent { ); } login() { + this.ga.event('Auth', 'Login', 'click'); this.auth.login(); } logout() { + this.ga.event('Auth', 'Logout', 'click'); this.auth.logout(); } } diff --git a/default/src/app/auth/auth.service.ts b/default/src/app/auth/auth.service.ts index 21477f0..8313513 100644 --- a/default/src/app/auth/auth.service.ts +++ b/default/src/app/auth/auth.service.ts @@ -8,10 +8,7 @@ import { Observable, BehaviorSubject } from 'rxjs/Rx'; import { User } from '../shared/user'; import { BackendService } from '../shared/backend.service'; import { Integration } from '../shared/integration'; - - -// Declare ga function as ambient -declare var ga: Function; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; @Injectable() export class AuthService { @@ -28,7 +25,7 @@ export class AuthService { } ); - constructor(private fb_auth: AngularFireAuth, private backend: BackendService) { + constructor(private fb_auth: AngularFireAuth, private backend: BackendService, private ga: GoogleAnalyticsService) { this.fb_auth.subscribe( // An auth event happened. @@ -63,10 +60,10 @@ export class AuthService { this.getUser().subscribe( (user: User) => { if (user && user.loggedIn) { - ga('set', 'userId', user.uid); // Set the user ID using signed-in user_id. + this.ga.setUserId(user.uid); // Set the user ID using signed-in user_id. } if (user && !user.loggedIn) { - ga('unset', 'userId', null); + this.ga.unsetUserId(); } } ); diff --git a/default/src/app/report/report.component.ts b/default/src/app/report/report.component.ts index 000cd9f..7fbf820 100644 --- a/default/src/app/report/report.component.ts +++ b/default/src/app/report/report.component.ts @@ -5,6 +5,7 @@ import { Component } from '@angular/core'; import { QueryService } from '../query/query.service'; import { AuthService } from '../auth/index'; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; // Note that this also imports moment itself. import * as moment from 'moment-timezone'; @@ -46,7 +47,7 @@ export class ReportComponent { moment.tz.setDefault(this.tz); } - constructor(private queryService: QueryService, private auth: AuthService) { + constructor(private queryService: QueryService, private auth: AuthService, private ga: GoogleAnalyticsService) { this.setTimezone(); this.auth.getUser().subscribe( @@ -95,6 +96,7 @@ export class ReportComponent { } getMoreData(cursor: string) { + this.ga.event('Report', 'Show more', 'click'); this.queryService.getQueries(cursor).subscribe( (data: any[]) => { this.data = this.data.concat(this.processData(data['payload'], data['cursor'])); diff --git a/default/src/app/search/search.component.ts b/default/src/app/search/search.component.ts index 33bb449..d51ffdb 100644 --- a/default/src/app/search/search.component.ts +++ b/default/src/app/search/search.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { QueryService } from '../query/index'; import {AuthService} from '../auth/auth.service'; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; @Component({ selector: 'search', @@ -12,7 +13,7 @@ import {AuthService} from '../auth/auth.service'; export class SearchComponent { private preSearchText: any; - constructor(private queryService: QueryService, private auth: AuthService) { + constructor(private queryService: QueryService, private auth: AuthService, private ga: GoogleAnalyticsService) { this.preSearchText = this.populateSearch(window.location.href); this.recordOmniSearch(window.location.href); } @@ -20,16 +21,19 @@ export class SearchComponent { submit(searchField: string) { if (searchField !== '') { this.doSearch(searchField) ; + this.ga.event('Search', 'submit', 'clicking submit button'); } } onPressEnter(e: any, searchField: any) { if (e.keyCode === 13 && searchField !== '') { this.doSearch(searchField); + this.ga.event('Search', 'submit', 'pressing enter'); } } doSearch(searchField: any) { + this.ga.event('Search', 'source', 'site-search'); this.queryService.doQuery(searchField, 'site-search').subscribe( data => { // If when data is returned from a query with a redirect set, do the redirect. @@ -50,6 +54,7 @@ export class SearchComponent { if (parameters['q'] != null) { this.queryService.doQuery(parameters['q'], 'omnibox').subscribe( response => { + this.ga.event('Search', 'source', 'omnibox'); return; }, error => { console.log('Error happened: ' + error); diff --git a/default/src/app/shared/google.analytics.service.ts b/default/src/app/shared/google.analytics.service.ts new file mode 100644 index 0000000..d986719 --- /dev/null +++ b/default/src/app/shared/google.analytics.service.ts @@ -0,0 +1,15 @@ +declare var ga: Function; + +export class GoogleAnalyticsService { + + public event(eventCategory: string, eventAction: string, eventLabel: string) { + ga('send', 'event', eventCategory, eventAction, eventLabel); + } + public setUserId(userId: string) { + ga('set', 'userId', userId); + } + public unsetUserId() { + ga('unset', 'userId', null); + } + +} From 93574836a7b256a592218e2c4579cde479c1ef44 Mon Sep 17 00:00:00 2001 From: Aashil Date: Tue, 13 Sep 2016 17:40:24 -0400 Subject: [PATCH 2/3] PROD-276: Create MockGoogleAnalyticsService in the tests in order to mock the actual implementation of the methods which record the events. * We do this to not record the events for tests. --- default/src/app/auth/auth-button.component.spec.ts | 13 ++++++++++++- default/src/app/report/report.component.spec.ts | 13 ++++++++++++- default/src/app/search/search.component.spec.ts | 13 ++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/default/src/app/auth/auth-button.component.spec.ts b/default/src/app/auth/auth-button.component.spec.ts index 7c2cc32..46d88fa 100644 --- a/default/src/app/auth/auth-button.component.spec.ts +++ b/default/src/app/auth/auth-button.component.spec.ts @@ -12,6 +12,7 @@ import { AuthService } from './auth.service'; import { AuthButtonComponent } from './auth-button.component'; import {BehaviorSubject} from 'rxjs/Rx'; import { User } from '../shared/user'; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; @@ -35,11 +36,21 @@ class MockAuthService { } +class MockGoogleAnalyticsService { + event(eventCategory: string, eventAction: string, eventLabel: string) { + } + setUserId(userId: string) { + } + unsetUserId() { + } +} + describe('AuthButtonComponent', () => { beforeEach(() => { addProviders([ AuthButtonComponent, - {provide: AuthService, useClass: MockAuthService} + {provide: AuthService, useClass: MockAuthService}, + {provide: GoogleAnalyticsService, useClass: MockGoogleAnalyticsService} ]); }); diff --git a/default/src/app/report/report.component.spec.ts b/default/src/app/report/report.component.spec.ts index e628d82..6d45d9d 100644 --- a/default/src/app/report/report.component.spec.ts +++ b/default/src/app/report/report.component.spec.ts @@ -4,6 +4,7 @@ import { QueryService } from '../query/index'; import { AuthService} from '../auth/auth.service'; import { Observable, BehaviorSubject, Observer } from 'rxjs'; import { User } from '../shared/user'; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; // Note that this also imports moment itself. import * as moment from 'moment-timezone'; @@ -38,6 +39,15 @@ class MockAuthService { } +class MockGoogleAnalyticsService { + event(eventCategory: string, eventAction: string, eventLabel: string) { + } + setUserId(userId: string) { + } + unsetUserId() { + } +} + // TODO: This isn't actually testing the component on the webpage, but is calling the items directly. // TODO: RC5 adds TestBed Class.. see // TODO: We can use https://developers.livechatinc.com/blog/testing-angular-2-apps-dependency-injection-and-components/ @@ -46,7 +56,8 @@ describe('SearchComponent', () => { addProviders([ ReportComponent, {provide: QueryService, useClass: MockQueryService}, - {provide: AuthService, useClass: MockAuthService} + {provide: AuthService, useClass: MockAuthService}, + {provide: GoogleAnalyticsService, useClass: MockGoogleAnalyticsService} ]); }); it('should not call QueryService.getQueries() when user is logged out.', diff --git a/default/src/app/search/search.component.spec.ts b/default/src/app/search/search.component.spec.ts index 7220fd6..114089c 100644 --- a/default/src/app/search/search.component.spec.ts +++ b/default/src/app/search/search.component.spec.ts @@ -4,6 +4,7 @@ import { QueryService } from '../query/index'; import { AuthService} from '../auth/auth.service'; import { Observable, BehaviorSubject, Observer } from 'rxjs'; import { User } from '../shared/user'; +import { GoogleAnalyticsService } from '../shared/google.analytics.service'; class MockQueryService { @@ -38,6 +39,15 @@ class MockAuthService { } +class MockGoogleAnalyticsService { + event(eventCategory: string, eventAction: string, eventLabel: string) { + } + setUserId(userId: string) { + } + unsetUserId() { + } +} + // TODO: This isn't actually testing the component on the webpage, but is calling the items directly. // TODO: RC5 adds TestBed Class.. see // TODO: We can use https://developers.livechatinc.com/blog/testing-angular-2-apps-dependency-injection-and-components/ @@ -46,7 +56,8 @@ describe('SearchComponent', () => { addProviders([ SearchComponent, {provide: QueryService, useClass: MockQueryService}, - {provide: AuthService, useClass: MockAuthService} + {provide: AuthService, useClass: MockAuthService}, + {provide: GoogleAnalyticsService, useClass: MockGoogleAnalyticsService} ]); }); it('submit button should NOT send a query if search field is empty', From 85b1f73eb135e6d16ebc7ea866119aca42a4f66f Mon Sep 17 00:00:00 2001 From: Aashil Date: Wed, 14 Sep 2016 10:36:08 -0400 Subject: [PATCH 3/3] PROD-276: Write tests for recording the Google Analytics events at specific times. * Use spyOn and expect the GA method to have been called with specific parameters. --- .../app/auth/auth-button.component.spec.ts | 13 +++++ .../src/app/report/report.component.spec.ts | 29 ++++++++++ .../src/app/search/search.component.spec.ts | 53 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/default/src/app/auth/auth-button.component.spec.ts b/default/src/app/auth/auth-button.component.spec.ts index 46d88fa..30f1b46 100644 --- a/default/src/app/auth/auth-button.component.spec.ts +++ b/default/src/app/auth/auth-button.component.spec.ts @@ -75,4 +75,17 @@ describe('AuthButtonComponent', () => { expect(authButtonComponent.loggedIn).toBe(false); })); + + it('should record GA event for login and logout', inject( + // Note that we pass AuthService, NOT MockAuthService + [AuthButtonComponent, GoogleAnalyticsService], (authButtonComponent: AuthButtonComponent, + ga: MockGoogleAnalyticsService + ) => { + + spyOn(ga, 'event'); + authButtonComponent.login(); + expect(ga.event).toHaveBeenCalledWith('Auth', 'Login', 'click'); + authButtonComponent.logout(); + expect(ga.event).toHaveBeenCalledWith('Auth', 'Logout', 'click'); + })); }); diff --git a/default/src/app/report/report.component.spec.ts b/default/src/app/report/report.component.spec.ts index 6d45d9d..187b46d 100644 --- a/default/src/app/report/report.component.spec.ts +++ b/default/src/app/report/report.component.spec.ts @@ -292,5 +292,34 @@ describe('SearchComponent', () => { } ) ); + + it('should record GA event when clicking Show more button', + inject([ReportComponent, QueryService, GoogleAnalyticsService], + (component: ReportComponent, querySrv: QueryService, ga: MockGoogleAnalyticsService) => { + + spyOn(querySrv, 'getQueries').and.callFake(() => { + return Observable.create( + (observer: Observer) => { + observer.next({ + 'cursor': 'fake', + 'payload': [ + { + 'query': 'asdfasdf', + 'tags': ['fake-tag'], + 'timestamp': '2016-09-01T22:04:38.787362' + } + ] + }); + observer.complete(); + } + ); + }); + + spyOn(ga, 'event'); + component.getMoreData('cursor_coming_through'); + expect(ga.event).toHaveBeenCalledWith('Report', 'Show more', 'click'); + } + ) + ); }); }); diff --git a/default/src/app/search/search.component.spec.ts b/default/src/app/search/search.component.spec.ts index 114089c..605eeea 100644 --- a/default/src/app/search/search.component.spec.ts +++ b/default/src/app/search/search.component.spec.ts @@ -117,4 +117,57 @@ describe('SearchComponent', () => { }) ); + + it('should record the GA event for submit button on clicking it or pressing enter key', + inject([SearchComponent, GoogleAnalyticsService], (component: SearchComponent, + ga: MockGoogleAnalyticsService + ) => { + spyOn(component, 'doSearch').and.callFake(() => { + return Observable.create( + (observer: Observer) => { + observer.next({ + 'success': 'true', + 'payload': { + 'redirect': 'http://google.com/q#=whatever', + }, + 'cursor': null + }); + observer.complete(); + } + ); + }); + spyOn(ga, 'event'); + component.submit('whatever'); + expect(ga.event).toHaveBeenCalledWith('Search', 'submit', 'clicking submit button'); + component.onPressEnter({'keyCode' : 13}, 'whatever'); + expect(ga.event).toHaveBeenCalledWith('Search', 'submit', 'pressing enter'); + }) + ); + + it('should record the GA event for source', + inject([SearchComponent, QueryService, GoogleAnalyticsService], (component: SearchComponent, + querySrv: MockQueryService, + ga: MockGoogleAnalyticsService + ) => { + spyOn(querySrv, 'doQuery').and.callFake(() => { + return Observable.create( + (observer: Observer) => { + observer.next({ + 'success': 'true', + 'payload': { + 'redirect': 'http://google.com/q#=whatever', + }, + 'cursor': null + }); + observer.complete(); + } + ); + }); + spyOn(ga, 'event'); + component.doSearch('whatever'); + expect(ga.event).toHaveBeenCalledWith('Search', 'source', 'site-search'); + component.recordOmniSearch('http://localhost:8080/?q=hello'); + expect(ga.event).toHaveBeenCalledWith('Search', 'source', 'omnibox'); + }) + ); });