July 18, 2018
Angular, Preserving Query Params and Google AdWords
… and how all this leads to an incorrect source of traffic in Google Analytics and incorrect conversion attribution.
The situation is as follows:
On www.thecodecampus.de we run an Angular 6 page. We advertise our product with Google AdWords. If a visitor comes via Google AdWords, the URL is appended with a glcid and several campaign parameters as query params. This data is important for tracking, as Google Analytics determines to which source a conversion/visit is to be attributed.
Without this information, we cannot determine (in Google Analytics) whether a visit or booking was made through Google AdWords or any other source. The data is accordingly important to us. Unfortunately, Angular is configured by default to remove any query parameters when navigating.
This means that the GCLID is still present on the initial page, but if the user clicks on another route within the Angular application, the URL is removed and the data is lost.
In order to fix this problem you can either use queryParamsHandling
on each router.navigate() call and on each routerLink. But that is not practical. Another option is to use a directive that updates each routerLink accordingly with the current query parameters as shown here. But this solution doesn’t cover router.navigate() calls from component classes.
1 2 3 4 5 6 7 8 9 10 |
@Directive({ selector: 'a[routerLink]' }) export class PreserveQueryParamsDirective { constructor(private link: RouterLinkWithHref, private route: ActivatedRoute ) { this.route.queryParamMap.subscribe(queryParams => { this.link.queryParams = Object.assign({}, this.route.snapshot.queryParams); }); } } |
Another solution is to use guards until the issue in the official repository is resolved. But this requires you to set runGuardsAndResolvers for the guard to always and to cancel all navigation requests and is a very hacky solution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import {Injectable} from '@angular/core'; import {ActivatedRouteSnapshot, CanDeactivate, Router, RouterStateSnapshot} from '@angular/router'; import {Observable} from 'rxjs/Observable'; import {ParamMap} from '@angular/router/src/shared'; @Injectable() export class QueryGuard implements CanDeactivate<any> { constructor(private router: Router) { } /** * Well, why do I exist? * * Angular strips query params during route change and we lost a URL parameter that is required for Google AdWords. * Therefore we prevent any route change where the currentState has query params and the nextState does not. Then * cancelling the original navigation attempt and starting a new one with the queryParamsHandling set to preserve. * * Currently I am only slightly tested and may break existing logic like route rejection, data and so on. * */ canDeactivate(component: any, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { // if there are no query params on the current route - do nothing. if (currentRoute.root.queryParamMap.keys.length === 0) { return true; } if (nextState) { // if (nextState.root.queryParamMap.has(currentState.root.queryParamMap.keys[0])) { if (this.comapreParmMaps(currentState.root.queryParamMap, nextState.root.queryParamMap)) { // check only if the first key is present - then the guard has already done // his job and we return true in order to avoid endless loop. return true; } else { // Apparently the query params need to be added: this.router.navigate([nextState.url], { preserveFragment: true, queryParamsHandling: 'preserve' }); // Cancel initial navigation attempt return false; } } } /** * Compares two routes and returns true if all params exist in both routes*/ private comapreParmMaps(mapA: ParamMap, mapB: ParamMap): boolean { return mapA.keys .map(key => mapB.has(key)) .every(it => it); } } |