Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit 8c9dad4

Browse files
committed
Set up Google Analytics 4 along with keeping legacy Universal Analytics
We currently use Universal Analytics. This is deprecated in favor of Google Analytics 4 and UA will stop processing hits in October 2023. This change intends to prepare us for this migration, and to already pre-populate our GA4 property (there is no way to migrate existing data /properties into a GA4 property -- a new one needs to be created). This will help us minimize the data gap so that we can: * Continue to look at the UA property with the full time span until October 2023 * Can start using the GA4 property long-term in the future, starting with data even before Universal Analytics stops processing new data. We need to keep the existing `analytics.js` setup. Initially we have considered using `gtag.js` for both the UA and GA4 properties, as it supports that, but that doesn't work with our strict trusted types enforcement because it results in multiple `gtag.js` scripts (specific versions for UA or GA4) that recreate the same trusted type policies. This causes runtime errors and breaks the setup. Instead, with continued use of `analytics.js` we have the benefit of a good separation of trusted types + events and configuration. There is some problematic with translation of Universal Analytics Events to GA4, or the other way around (even though we don't use custom events currently) We also do not need to send page views for our GA4 property because GA4 with gtag supports this automatically (respecting the history state -- using the `Enhanced measurement events` setting in the UI). For our UA legacy instance we continue to dispatch events manually. This logic can be removed in the future. More details can be found here: https://docs.google.com/document/d/1aK8u4ZlXbqQ2wMqmgSX7Ces8iLgamC13oCoG6VeBruA/edit?usp=sharing&resourcekey=0-EVe-Rhnme3bj_pkz2RcOmw.
1 parent fedeef7 commit 8c9dad4

File tree

6 files changed

+123
-64
lines changed

6 files changed

+123
-64
lines changed

src/app/material-docs-app.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Component, OnDestroy, ViewEncapsulation} from '@angular/core';
22

3-
import {GaService} from './shared/ga/ga';
3+
import {AnalyticsService} from './shared/analytics/analytics';
44
import {NavigationFocusService} from './shared/navigation-focus/navigation-focus.service';
55
import {Subscription} from 'rxjs';
66
import {map, pairwise, startWith} from 'rxjs/operators';
@@ -14,19 +14,23 @@ import {map, pairwise, startWith} from 'rxjs/operators';
1414
export class MaterialDocsApp implements OnDestroy {
1515
private subscriptions = new Subscription();
1616

17-
constructor(ga: GaService, navigationFocusService: NavigationFocusService) {
18-
this.subscriptions.add(navigationFocusService.navigationEndEvents.pipe(
19-
map(e => e.urlAfterRedirects),
20-
startWith(''),
21-
pairwise()
22-
).subscribe(([fromUrl, toUrl]) => {
23-
// We want to reset the scroll position on navigation except when navigating within
24-
// the documentation for a single component.
25-
if (!navigationFocusService.isNavigationWithinComponentView(fromUrl, toUrl)) {
26-
resetScrollPosition();
27-
}
28-
ga.locationChanged(toUrl);
29-
}));
17+
constructor(analytics: AnalyticsService, navigationFocusService: NavigationFocusService) {
18+
this.subscriptions.add(
19+
navigationFocusService.navigationEndEvents
20+
.pipe(
21+
map(e => e.urlAfterRedirects),
22+
startWith(''),
23+
pairwise()
24+
)
25+
.subscribe(([fromUrl, toUrl]) => {
26+
// We want to reset the scroll position on navigation except when navigating within
27+
// the documentation for a single component.
28+
if (!navigationFocusService.isNavigationWithinComponentView(fromUrl, toUrl)) {
29+
resetScrollPosition();
30+
}
31+
analytics.locationChanged(toUrl);
32+
})
33+
);
3034
}
3135

3236
ngOnDestroy() {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {Injectable} from '@angular/core';
2+
3+
import {environment} from '../../../environments/environment';
4+
5+
/** Extension of `Window` with potential Google Analytics fields. */
6+
declare global {
7+
interface Window {
8+
dataLayer?: any[];
9+
gtag?(...args: any[]): void;
10+
/** Legacy Universal Analytics `analytics.js` field. */
11+
ga?(...args: any[]): void;
12+
}
13+
}
14+
15+
/**
16+
* Google Analytics Service - captures app behaviors and sends them to Google Analytics.
17+
*
18+
* Note: Presupposes that the legacy `analytics.js` script has been loaded on the
19+
* host web page.
20+
*
21+
* Associates data with properties determined from the environment configurations:
22+
* - Data is uploaded to a legacy Universal Analytics property.
23+
* - Data is uploaded to our main Google Analytics 4+ property.
24+
*/
25+
@Injectable({providedIn: 'root'})
26+
export class AnalyticsService {
27+
private previousUrl: string | undefined;
28+
29+
constructor() {
30+
this._installGlobalSiteTag();
31+
32+
// TODO: Remove this when we fully switch to Google Analytics 4+.
33+
this._legacyGa('create', environment.legacyUniversalAnalyticsMaterialId, 'auto', 'mat');
34+
this._legacyGa('create', environment.legacyUniversalAnalyticsMainId, 'auto', 'ng');
35+
}
36+
37+
locationChanged(url: string) {
38+
this._sendPage(url);
39+
}
40+
41+
private _sendPage(url: string) {
42+
// Won't re-send if the url hasn't changed.
43+
if (url === this.previousUrl) {
44+
return;
45+
}
46+
this.previousUrl = url;
47+
this._legacyGa('mat.set', 'page', url);
48+
this._legacyGa('ng.set', 'page', url);
49+
this._legacyGa('mat.send', 'pageview');
50+
this._legacyGa('ng.send', 'pageview');
51+
}
52+
53+
private _legacyGa(...args: any[]) {
54+
if (window.ga) {
55+
window.ga(...args);
56+
}
57+
}
58+
59+
private _installGlobalSiteTag() {
60+
const url =
61+
`https://www.googletagmanager.com/gtag/js?id=${environment.googleAnalyticsMaterialId}`;
62+
63+
// Note: This cannot be an arrow function as `gtag.js` expects an actual `Arguments`
64+
// instance with e.g. `callee` to be set. Do not attempt to change this and keep this
65+
// as much as possible in sync with the tracking code snippet suggested by the Google
66+
// Analytics 4 web UI under `Data Streams`.
67+
window.dataLayer = window.dataLayer || [];
68+
window.gtag = function() {
69+
window.dataLayer?.push(arguments);
70+
};
71+
window.gtag('js', new Date());
72+
73+
// Configure properties before loading the script. This is necessary to avoid
74+
// loading multiple instances of the gtag JS scripts.
75+
window.gtag('config', environment.googleAnalyticsOverallDomainId);
76+
window.gtag('config', environment.googleAnalyticsMaterialId);
77+
78+
// skip `gtag` for Protractor e2e tests.
79+
if (window.name.includes('NG_DEFER_BOOTSTRAP')) {
80+
return;
81+
}
82+
83+
const el = window.document.createElement('script');
84+
el.async = true;
85+
el.src = url;
86+
window.document.head.appendChild(el);
87+
}
88+
}

src/app/shared/carousel/carousel.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y';
1616
import {LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes';
1717
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
1818

19-
2019
@Directive({
2120
selector: '[carousel-item]',
2221
})
@@ -35,7 +34,7 @@ export class CarouselItem implements FocusableOption {
3534
selector: 'app-carousel',
3635
templateUrl: './carousel.html',
3736
styleUrls: ['./carousel.scss'],
38-
encapsulation: ViewEncapsulation.None
37+
encapsulation: ViewEncapsulation.None,
3938
})
4039
export class Carousel implements AfterContentInit {
4140
@Input('aria-label') ariaLabel: string | undefined;
@@ -145,7 +144,9 @@ export class Carousel implements AfterContentInit {
145144
return true;
146145
}
147146

148-
return (!side || side === 'end') &&
149-
(offsetWidth + offsetLeft - this.position) > this.list.nativeElement.clientWidth;
147+
return (
148+
(!side || side === 'end') &&
149+
offsetWidth + offsetLeft - this.position > this.list.nativeElement.clientWidth
150+
);
150151
}
151152
}

src/app/shared/ga/ga.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export const environment = {
2-
matGaId: 'UA-8594346-24', // Production id
3-
ngGaId: 'UA-8594346-15', // Production id
2+
googleAnalyticsOverallDomainId: 'G-BVV0RDSG7F', // Production framework property id.
3+
googleAnalyticsMaterialId: 'G-XXPDW812XH', // Production Material id
4+
5+
legacyUniversalAnalyticsMainId: 'UA-8594346-15', // Legacy production id
6+
legacyUniversalAnalyticsMaterialId: 'UA-8594346-24', // Legacy production id
7+
48
production: true,
59
};

src/environments/environment.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
// The list of which env maps to which file can be found in `angular-cli.json`.
55

66
export const environment = {
7-
matGaId: '', // No development id for Material
8-
ngGaId: 'UA-8594346-26', // Development id
7+
googleAnalyticsOverallDomainId: 'G-Q8PB6PJ5CC', // Development framework property id.
8+
googleAnalyticsMaterialId: 'G-8DL3XGKYMC', // Development Material id
9+
10+
legacyUniversalAnalyticsMainId: 'UA-8594346-26', // Legacy development id
11+
legacyUniversalAnalyticsMaterialId: '', // No legacy development id for Material
12+
913
production: false,
1014
};
1115

0 commit comments

Comments
 (0)