Skip to content

Commit

Permalink
refactor: organized analytics and consent services, styled consent co…
Browse files Browse the repository at this point in the history
…mponents

refactored consent and analytics to load config from configservice. Added better explanations and
examples of the various cookies types. Cleaned up the styling of the consent window. Includes fixes by @jonrkarr

closes #3790

Co-authored-by: Jonathan Karr <jonrkarr@gmail.com>
  • Loading branch information
bilalshaikh42 and jonrkarr committed Dec 16, 2021
1 parent c07cfdd commit 9da76a1
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 162 deletions.
13 changes: 0 additions & 13 deletions apps/platform/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,6 @@
analytics_storage: 'denied',
advertising_storage: 'denied',
});

// Make sure any changes are properly reflected in the privacy policy/cookie policy.

// Disable all advertising/tracking
gtag('set', 'allow_google_signals', false);

gtag('config', 'G-G3CVBC0V5N', {
send_page_view: false,
cookie_prefix: 'goog_analytics_perf_',
link_attribution: {
cookie_name: 'goog_analytics_la_perf',
},
});
</script>
<meta
name="description"
Expand Down
1 change: 1 addition & 0 deletions libs/analytics/angular-analytics/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@typescript-eslint/explicit-member-accessibility": "error",
"@angular-eslint/directive-selector": [
"error",
{
Expand Down
37 changes: 35 additions & 2 deletions libs/analytics/angular-analytics/src/lib/analytics.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
import { TestBed } from '@angular/core/testing';

import { AnalyticsService } from './analytics.service';
import {
MatDialogModule,
MAT_DIALOG_DATA,
MatDialogRef,
} from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { Storage } from '@ionic/storage-angular';
import { ConfigService } from '@biosimulations/config/angular';
import { RouterTestingModule } from '@angular/router/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

describe('AnalyticsService', () => {
let service: AnalyticsService;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
imports: [
MatDialogModule,
MatExpansionModule,
MatSlideToggleModule,
RouterTestingModule,
NoopAnimationsModule,
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: undefined,
},
Storage,
{
provide: ConfigService,
useValue: {
appName: 'testApp',
analyticsId: 'testId',
},
},
],
});
service = TestBed.inject(AnalyticsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
Expand Down
89 changes: 75 additions & 14 deletions libs/analytics/angular-analytics/src/lib/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* @ File - analytics.service.ts - Client to google analytics.
* TODO : Manages consent automatically.
*
* @ File - analytics.service.ts - Client to google analytics. Automatically manages consent.
* @ Author -Bilal Shaikh
* Inspired heavily by https://www.dottedsquirrel.com/google-analytics-angular/
*/
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ConfigService } from '@biosimulations/config/angular';
import { Subscription } from 'rxjs';
import { ConsentService } from './consent.service';
import { Consent } from './datamodel';

Expand All @@ -16,34 +16,95 @@ declare let gtag: Function;
providedIn: 'root',
})
export class AnalyticsService {
private routerSubscription: Subscription | null = null;
private initPageView = false;
private analyticsId: string;
public constructor(
public router: Router,
private route: ActivatedRoute,
private config: ConfigService,

private consentService: ConsentService,
) {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
gtag('set', 'page', event.urlAfterRedirects);
gtag('send', 'pageview');
}
});
this.analyticsId = this.config.analyticsId;
// Make sure any changes are properly reflected in the privacy policy/cookie policy.
// Ordering is important here.

this.consentService.consent$.subscribe((consent: Consent) => {
// This should be first so we don't store any cookies until/unless we get consent
this.consentService.consent$.pipe().subscribe((consent: Consent) => {
gtag('consent', 'update', consent);
// Disable all advertising/tracking
gtag('set', 'allow_google_signals', false);

const send_pageview = consent.analytics_storage === 'granted';
this.init(send_pageview);

gtag('event', 'consent_recorded', {
eventValue: consent.analytics_storage,
eventLabel: 'consent_recorded',
});
});
}

public async init(sendPageView: boolean): Promise<void> {
// page views will be handled by analytics service. Give cookies clear names to ref in privacy policy.

console.error('init');
gtag('config', this.analyticsId, {
send_page_view: false,
cookie_prefix: 'goog_analytics_perf_',
link_attribution: {
cookie_name: 'goog_analytics_la_perf',
},
});

// Only want to have one subscription to the router
if (!this.routerSubscription) {
console.error('subscribe');
this.routerSubscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
gtag('set', 'page_location', event.urlAfterRedirects);
// TODO get info from activated route
gtag('event', 'page_view');
}
});
}
// Only send page view once when module loads
if (sendPageView && !this.initPageView) {
gtag('event', 'page_view');
this.initPageView = true;
}
}

// https://developers.google.com/analytics/devguides/collection/gtagjs/events
//https://support.google.com/analytics/answer/1033068#Anatomy&zippy=%2Cin-this-article
public pageviewEmitter(
page_location: string,
page_path: string,
page_title: string,
): void {
gtag('page_view', {
page_location,
page_path,
page_title,
});
}

public eventEmitter(
eventName: string,
eventCategory: string,
eventCategory: EventCategory,
eventAction: string,
eventLabel: string | null = null,
eventValue: number | null = null,
) {
nonInteraction: boolean = false,
): void {
gtag('event', eventName, {
eventCategory: eventCategory,
eventLabel: eventLabel,
eventAction: eventAction,
eventValue: eventValue,
value: eventValue,
non_interaction: nonInteraction,
});
}
}
type EventCategory = 'engagement' | 'custom';
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ import { MatButtonModule } from '@angular/material/button';
],
providers: [AnalyticsService],
declarations: [CookieConsentComponent],
exports: [CookieConsentComponent],
})
export class AngularAnalyticsModule {
constructor(
public constructor(
// Needs to be imported so DI can do its thing and run the constructors
private consentService: ConsentService,
// Needs to be imported so DI can do its thing and run the constructor
private analyticsService: AnalyticsService,
) {
this.consentService.initConsent();
}
) {}
}
34 changes: 33 additions & 1 deletion libs/analytics/angular-analytics/src/lib/consent.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
import { TestBed } from '@angular/core/testing';

import { ConsentService } from './consent.service';
import {
MatDialogModule,
MAT_DIALOG_DATA,
MatDialogRef,
} from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { Storage } from '@ionic/storage-angular';
import { ConfigService } from '@biosimulations/config/angular';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

describe('ConsentService', () => {
let service: ConsentService;

beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
imports: [
MatDialogModule,
MatExpansionModule,
MatSlideToggleModule,
NoopAnimationsModule,
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: undefined,
},
Storage,
{
provide: ConfigService,
useValue: {
appName: 'testApp',
analyticsId: 'testId',
},
},
],
});
service = TestBed.inject(ConsentService);
});

Expand Down
20 changes: 10 additions & 10 deletions libs/analytics/angular-analytics/src/lib/consent.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Consent, ConsentRecord } from './datamodel';
import { Consent, ConsentRecord, cookieConsentType } from './datamodel';
import { Storage } from '@ionic/storage-angular';

import { CookieConsentComponent } from './cookie-consent/cookie-consent.component';
import { MatDialog } from '@angular/material/dialog';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -45,15 +45,15 @@ export class ConsentService {
}

private startConsentFlow(): void {
const dialogRef = this.dialog.open(CookieConsentComponent, {
hasBackdrop: true,
maxWidth: '900px',

disableClose: true,
closeOnNavigation: false,
});
const dialogRef: MatDialogRef<CookieConsentComponent, cookieConsentType> =
this.dialog.open(CookieConsentComponent, {
hasBackdrop: true,
maxWidth: '900px',
disableClose: true,
closeOnNavigation: false,
});
dialogRef.afterClosed().subscribe((result) => {
if (result && result.performanceCookies) {
if (result && result.performance) {
const consentRecord: ConsentRecord = {
date: new Date().toISOString(),
consent: {
Expand Down

0 comments on commit 9da76a1

Please sign in to comment.