Parallels Desktop for Mac - Automatically Map a Drive With Administrator Privileges

I’ve been using Windows my whole life.

Then, in 2018, I made the jump to Mac.
In order to save me a bit of trouble I invested in Parallels Desktop for Mac to support clients with .NET systems.

Problem

I needed my Parallels Windows Virtual Machine to access some files which lives on my Mac drive as Y:\.

When I run the Command Prompt without admin rights, everything works as advertised:

1
2
C:\Users\ruanbeukes>y:
Y:\>

When I run Command Prompt as administrator, computer says: “No…”:
(Computer says no clip)

1
2
3
C:\WINDOWS\system32>y:
The system cannot find the drive specified.
C:\WINDOWS\system32>

Solution

Create a batch file MapMacDrive.bat on your Windows drive:

1
2
3
net use y: \\Mac\Home
@echo \\Mac\Home should now be mapped and available for Administrator use too.
pause

Right click on MapMacDrive.bat and run as Administrator:

1
2
3
4
5
6
7
C:\WINDOWS\system32>net use y: \\Mac\Home
The command completed successfully.

\\Mac\Home should now be mapped and available for Administrator use too.

C:\WINDOWS\system32>pause
Press any key to continue . . .

Side note, to delete the drive create a batch file MapMacDriveDelete.bat and run as Administrator:

1
2
net use y: /delete
pause

Automate mapping on Windows restart

On a Windows restart, the mapped drive is gone and you’ll have to manually run the batch file again…not good enough.

I automate this by using the Windows Task Scheduler.

Open a run box by pressing Windows Key + R, then execute taskschd.msc.
Look for the Create Task action on the right panel.

Create a new task MapMacHomeDrive with…

General Tab

Make sure to select Run with highest privileges.

New Task General Tab image

Trigger Tab

Create a new Trigger which will run At log on.

New Trigger image

Actions Tab

Create a new Action which will run the batch file.

New Action image

Your drive will automagically be mapped on restart.

Use it…don’t use it :)

Angular Typesafe Reactive Forms Helper

Oct 2017 I wrote a post - Angular Typesafe Reactive Forms.

A few people started using the implementation and also suggested enhancements.

We started sharing code in the blog’s comments, then later I moved the code into a github gist angular-reactive-forms-helper.ts.
At least the gist file is one step better than comments, right? :)

Then, early last week one of my mates contacted me with a change request.
He created the suggested change in a gist file and shared it with me.
I then updated his changes into my gist.

I had enough, this thing should be Open Source so that everyone can contribute.

It only took me two and a half years to finally move the code to github…LOL

Thus, npm package angular-typesafe-reactive-forms-helper was born.

Now we all can make it better!

npm package: angular-typesafe-reactive-forms-helper
github: rpbeukes/angular-typesafe-reactive-forms-helper

Use it…don’t use it :)

PS: Hope you and your families keep safe during this crazy COVID-19 times.
We will get through this.

How I Discovered Monkey Patching While Mocking AWS Cognito in an Angular App

I’ve been using AWS Cognito as the authentication piece to give users access to an Angular web project I’m working on.
Everything worked as advertised and I was happy with the result, until I started testing my authentication service.
That’s when things got interesting and I could not create a spy object on any of the Cognito classes.

Instead, I got…

1
2
3
4
5
6
7
8
9
10
AuthenticationService when .signInUser is working normally instantiate cognito AuthenticationDetails with correct user credentials FAILED
Error: <spyOn> : AuthenticationDetails is not declared writable or has no setter
Usage: spyOn(<object>, <methodName>)
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/shared/services/auth/authentication.service.spec.ts:33:40)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:359:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:308:1)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:358:1)
at Zone.run (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:124:1)
at runInTestZone (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:561:1)

Quick Answer

1
2
3
4
5
6
7
8
import * as AWSCognito from 'amazon-cognito-identity-js';

Object.defineProperty(AWSCognito, 'AuthenticationDetails', {
writable: true,
value: 'foo'
});

spyAuthenticationDetails = spyOn(AWSCognito, 'AuthenticationDetails');
  • This is strange because the same code in node.js throws TypeError: Cannot redefine property: AuthenticationDetails.
  • Angular uses zone.js to modify, or extend, all your async APIs.
  • We call this Monkey patching.
  • In my case, Angular Monkey patched amazon-cognito-identity-js and the result is we can execute Object.defineProperty without TypeError: Cannot redefine property: AuthenticationDetails.
  • Mocking AWS Cognito is simple, now that I know :)
  • Code examples on how to mock AWS Cognito in Angular.

What is Object.defineProperty

Object.defineProperty is a function which is natively present in the Javascript runtime environment and takes the following arguments:

1
Object.defineProperty(obj, prop, descriptor)

With Object.defineProperty, one defines a new property directly on an object or modifies an existing property on an object.

I found a very good StackOverflow answer, How to use javascript Object.defineProperty, and here are some points which helped me:

In Javascript, standard properties (data member with getter and setter) are defined by accessor descriptor.
Exclusively, you can use data descriptor (so you can’t use value and set on the same property):

  • accessor descriptor = get + set
    • get must be a function; its return value is used in reading the property; if not specified, the default is undefined, which behaves like a function that returns undefined.
    • set must be a function; its parameter is filled with Right Hand Side in assigning a value to property; if not specified, the default is undefined, which behaves like an empty function.
  • data descriptor = value + writable
    • value default undefined; if writable, configurable and enumerable (see below) are true, the property behaves like an ordinary data field.
    • writable - default false; if not true, the property is read only; attempt to write is ignored without error**

Both descriptors can have these members:

  • configurable - default false; if not true, the property can’t be deleted; attempt to delete is ignored without error**
  • enumerable - default false; if true, it will be iterated in for(var i in theObject); if false, it will not be iterated, but it is still accessible as public.

(** unless in strict mode - in that case javascript stops execution with TypeError unless it is caught in try-catch block)

(Side note: Also good to explore Object.preventExtensions(), Object.seal(), Object.freeze())

In my case, I actually marked the Cognito.AuthenticationDetails constructor function as overridable (writable).
But, you can only do that if configurable: true.

Here is the Mozilla example Object.defineProperty to explore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
const object1 = {};

Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});

console.log(Object.getOwnPropertyDescriptor(object1, 'property1'));

// throws an error in strict mode
// Error: Cannot assign to read only property 'property1' of object '#<Object>'
object1.property1 = 77;

console.log(object1.property1);
// expected output: 42

If you want to explore my claim of read-only is the default, use console.log(Object.getOwnPropertyDescriptor(object1, 'property1'));.

Expected output:

1
Object { value: 42, writable: false, enumerable: false, configurable: false }

Let’s explore the configurable flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';
const object1 = {};

// define a property
Object.defineProperty(object1, 'property1', {
value: 42,
});

// let's redefine the property
Object.defineProperty(object1, 'property1', {
value: 88,
});

console.log(object1.property1);
// expected output: 88

The example above will throw error:

1
Error: Cannot redefine property: property1

Resolve this error with configurable: true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'use strict';
const object1 = {};

Object.defineProperty(object1, 'property1', {
value: 42,
configurable: true
});

// let's redefine the property
Object.defineProperty(object1, 'property1', {
value: 88,
});

console.log(object1.property1);
// expected output: 88

Background

I have an authentication.service.ts and I want to test signInUser.
I reference amazon-cognito-identity-js for the Cognito functionality.

Here are a few tests I’ve identified to be implemented:

  • Cognito.AuthenticationDetails instantiated with correct credentials.
  • Cognito.CognitoUserPool instantiated with the correct UserPoolId and ClientId.
  • Cognito.CognitoUser instantiated with the correct ICognitoUserData object.
  • cognitoUser.authenticateUser was called only once.
  • On successful login, username must be returned.

(I’ve identified these tests by the comment: // test - ... in authentication.service.ts)

Mocking Cognito

My first mission: Test Cognito.AuthenticationDetails instantiated with correct credentials.

Because I reference amazon-cognito-identity-js, I thought mocks and spies would be the tools for testing AuthenticationService.signInUser.

The code below should do the trick…

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
describe('AuthenticationService', () => {
let service: AuthenticationService;

beforeEach(() => {
// arrange
TestBed.configureTestingModule({});
service = TestBed.get(AuthenticationService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

describe('when .signInUser is working normally', () => {
let spyAuthenticationDetails: jasmine.Spy;
let result;

beforeEach(async () => {
//create spy
spyAuthenticationDetails = spyOn(AWSCognito, 'AuthenticationDetails');

// act
// system under test
service.signInUser({ userName: 'hello', password: 'password' }).subscribe(
value => {
result = value;
},
error => {
throw Error(error);
}
);
});

it('instantiate cognito AuthenticationDetails with correct user credentials', () => {
// assert
expect(spyAuthenticationDetails).toHaveBeenCalledTimes(1);
expect(spyAuthenticationDetails).toHaveBeenCalledWith({
Username: 'hello',
Password: 'password'
});
});
});
});

But failed with:

1
2
3
4
5
6
7
8
9
10
AuthenticationService when .signInUser is working normally instantiate cognito AuthenticationDetails with correct user credentials FAILED
Error: <spyOn> : AuthenticationDetails is not declared writable or has no setter
Usage: spyOn(<object>, <methodName>)
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/shared/services/auth/authentication.service.spec.ts:33:40)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:359:1)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:308:1)
at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:358:1)
at Zone.run (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:124:1)
at runInTestZone (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:561:1)

Based on the research here:

I’ve decided to go with the solution below:

1
2
3
4
Object.defineProperty(AWSCognito, 'AuthenticationDetails', {
writable: true,
value: 'foo'
});

I’ve updated my test and all was green…

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
describe('AuthenticationService', () => {
let service: AuthenticationService;

beforeEach(() => {
// arrange
TestBed.configureTestingModule({});
service = TestBed.get(AuthenticationService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

describe('when .signInUser is working normally', () => {
let spyAuthenticationDetails: jasmine.Spy;
let result;

beforeEach(async () => {
// Confusion: Why does this output `configurable: true` in Angular world but not in Node???
// outputs: Object{get: function() { ... }, set: undefined, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(AWSCognito, 'AuthenticationDetails')); configurable: true}

/********************* Magic *******************************/
Object.defineProperty(AWSCognito, 'AuthenticationDetails', {
writable: true,
value: 'foo'
});
/***********************************************************/

spyAuthenticationDetails = spyOn(AWSCognito, 'AuthenticationDetails');

// act
service.signInUser({ userName: 'hello', password: 'password' }).subscribe(
value => {
result = value;
},
error => {
throw Error(error);
}
);
});

it('instantiate cognito AuthenticationDetails with correct user credentials', () => {
// assert
expect(spyAuthenticationDetails).toHaveBeenCalledTimes(1);
expect(spyAuthenticationDetails).toHaveBeenCalledWith({
Username: 'hello',
Password: 'password'
});
});
});
});

Life was good and I continued working until the voice inside my head whispered:

“Why is this working…?”

Node behaves different to Angular

Although the chances of using a front-end library (amazon-cognito-identity-js) in a node.js server application is very low, I’ve created a quick test in node.js just to proof a point.

The test is following exactly what I’ve done in Angular, but without the extra package dependencies from Angular.

My investigation revealed Object.getOwnPropertyDescriptor(AWSCognito, 'AuthenticationDetails') returns configurable: false.

This is proof that AWS Cognito library is shipped with configurable: false.

Because it returns false, one would not be able to execute Object.defineProperty without an exception.
The AWS team does not want you to fiddle with their implementation of AWSCognito.AuthenticationDetails.

Below is my node.js test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const AWSCognito = require('amazon-cognito-identity-js');

describe('Investigaste Object.defineProperty of AWSCognito.AuthenticationDetails', () => {
it('It should throw "TypeError: Cannot redefine property: AuthenticationDetails" because Object.getOwnPropertyDescriptor returns configurable: false', () => {

console.log('Object.getOwnPropertyDescriptor(AWSCognito, "AuthenticationDetails"): ');
console.log(Object.getOwnPropertyDescriptor(AWSCognito, 'AuthenticationDetails'));

// this will throw exception because of 'configurable: false',
// meaning the AWS team does not want you to fiddle with this...and I agree, you shouldn't.
Object.defineProperty(AWSCognito, 'AuthenticationDetails', {
writable: true,
value: 'foo'
});

console.log(Object.getOwnPropertyDescriptor(AWSCognito, 'AuthenticationDetails'));
});

});

Output after running the test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Randomized with seed 26638
Started
Object.getOwnPropertyDescriptor(AWSCognito, "AuthenticationDetails"):
{ get: [Function: get],
set: undefined,
enumerable: true,
configurable: false }
F

Failures:
1) Investigaste Object.defineProperty of AWSCognito.AuthenticationDetails It should throw "TypeError: Cannot redefine property: AuthenticationDetails" because Object.getOwnPropertyDescriptor returns configurable: false
Message:
TypeError: Cannot redefine property: AuthenticationDetails
Stack:
at <Jasmine>
at UserContext.it (/Users/ruanbeukes/repos/MockCognitoInAngular/node-property-descriptor-investigation/test.spec.js:11:12)
at <Jasmine>
at runCallback (timers.js:705:18)
at tryOnImmediate (timers.js:676:5)

1 spec, 1 failure
Finished in 0.011 seconds
Randomized with seed 26638 (jasmine --random=true --seed=26638)

I’m confused…I’m sure in my Angular test, before the property modification code, I logged the exact same thing and the result was configurable: true.

How is that possible?!

Angular’s Zone.js and Monkey patching

Angular uses zone.js to handle change detection.
Because we have async code, Angular will Monkey patch all async APIs in order to managed the change detection.
(See resources below for a more in-depth look at zone'js as it does more than just change detection, it also helps with debugging)

A quick google revealed:

What is Monkey patching?
By definition, Monkey patching is basically extending or modifying the original API.
Now, zone.js re-defines all the async APIs like browser apis which includes set/clearTimeOut, set/clearInterval, alert, XHR apis etc.

amazon-cognito-identity-js will be re-defined by zone.js and also define the properties as configurable: true.
Because it sets configurable: true, it explains why I can define/modify object properties in the Angular world but not in the node.js world.

Below is the code that changes configurable: false to configurable: true from zone-evergreen.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function rewriteDescriptor(obj, prop, desc) {
// issue-927, if the desc is frozen, don't try to change the desc
if (!Object.isFrozen(desc)) {

/******** Ruan: This is where Angular does something different compared to node.js **/
desc.configurable = true;
/************************************************************************************/
}
if (!desc.configurable) {
// issue-927, if the obj is frozen, don't try to set the desc to obj
if (!obj[unconfigurablesKey] && !Object.isFrozen(obj)) {
_defineProperty(obj, unconfigurablesKey, { writable: true, value: {} });
}
if (obj[unconfigurablesKey]) {
obj[unconfigurablesKey][prop] = true;
}
}
return desc;
}

Here is the stack trace when I’ve added a conditional breakpoint prop === 'AuthenticationDetails':

1
2
3
4
5
6
7
8
9
10
11
12
13
rewriteDescriptor	                                   @ zone-evergreen.js:2227
Object.defineProperty @ zone-evergreen.js:2178
__webpack_require__.d @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:98
./src/shared/services/auth/authentication.service.ts @ authentication.service.spec.ts:201
__webpack_require__ @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:79
./src/shared/services/auth/authentication.service.spec.ts @ environment.ts:13
__webpack_require__ @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:79
webpackContext @ src sync \.spec\.ts$:9
./src/test.ts @ test.ts:20
__webpack_require__ @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:79
checkDeferredModules @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:45
(anonymous) @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:152
(anonymous) @ http://localhost:9876/_karma_webpack_/webpack/bootstrap:152

The rewriteDescriptor function can be found in angular/packages/zone.js/lib/browser/define-property.ts in the Angular repository.

Conclusion

  • Be aware that Angular uses zone.js to modify, or extend, all your async APIs.
  • We call this Monkey patching and it is Angular’s way to deal with change detection.
  • If you build an Angular app and have unexpected side-effects in one of your async API libraries, start your investigation with zone.js.

Hopefully it saved you a few hours of research.

Use it…don’t use it :)

Code Samples

Mock Cognito In Angular

Resources

Create and Push Your Local Git Repository With Command Line Tool - Hub

What is hub (hub source)?

hub is an extension to command-line git that helps you do everyday GitHub tasks without ever leaving the terminal.

Why do I need hub?

It saved me the trouble to sign into Github, create the repository manually, just so I can clone it and work locally.

Current Workflow

Usually when I start a new Github repository, I’ll sign in and then create the new repository. The repository will then show me instructions on how to clone, HTTPS or SSH. Depending on my setup, I’ll clone and start coding.

My Use Case

I started a small throw-away-project, with a local git repository. A few hours later, I decided that the code might be useful and I wanted to formally push it to a remote source control server like Github.

My dream was to drop to the terminal and git -u origin master, that should create and push my local repository to my Github account…of course it is not that simple :)

I searched for a github cli tool and hub came up.
It runs on macOS, Linux, Windows and a few others.

After I installed it, these are the steps I followed to create and push my local repository:

  1. Open terminal and navigate to source code.
  2. Enter: hub create

hub requested my github username.
hub requested my github password.
hub requested my Two-factor authentication code.

After the hub create command, I had a repository in Github…magic :)

  1. git push -u origin master (yes, my dream line becomes a reality)

I also received a Github email notification: [GitHub] A personal access token has been added to your account

Conclusion

Although I only used the create command, I would encourage you to check out the hub manual for more interesting commands.

These two commands are the magic:

hub create
git push -u origin master

Use it…don’t use it :)

My "Reading" Celebrations 2019

September 2018, exactly a year ago, I got this crazy idea to read one book a month for the next year.
It is crazy because I’m not really your A+ student and I can always find something better to do, like sleeping, before I’ll read for fun :)
The only time you’ll find me reading is when I’m doing some research on a coding problem or when I’m learning some new tech to put into practice.

Basically, I read for educational purposes…not for fun :)

Which brings me to my next problem, how do one hack the system if you don’t like reading?

You turn to audio books.

I signed up for an Amazon Audible account and started “reading” away.
It’s a subscription model and your monthly fee provides you one book a month.

I vowed not to sacrifice my family’s time and added my own rule: Listen only on my daily commute to work.
That is, 30-35 minutes of learning pleasure a day, 5 days a week.

I set out to find anything to drive my career forward, topics like: people and success, body language, goal setting, negotiation skills and leadership.

I was very focused…first month, done.
Second month, done.
Third month, something odd is happening…

The longer my journey continued, the more I discovered all these things I thought would drive my career, actually helped me more in my personal life.
I realised all the extra time I put in was an investment into my family.
It helped me be a better husband, dad and just be a better person in general.

Books

Below is a list of the books I read.
I’m not going to give you a book review, you can read the review on Amazon :)
Instead, I’ll highlight parts which I found useful in my life.

  1. (PG) The Magic of Thinking Big - David Schwartz

This book addressed my self doubt and changed my thinking to: I can do it.
In fact, it pushed me to apply for a job I would never have considered before…and I got the job :)

  1. (A) Influence: The Psychology of Persuasion - Robert B. Cialdini

You can apply this book to all aspects of your life.
I applied some of the principles on my kids and was pleasantly surprised :)
Word of caution, there is a fine line between influence and manipulation.
Use your super power for good.

  1. (PG) Extreme Ownership - Jocko Willink, Leif Babin

Two US Marines sharing their leadership knowledge and how to apply the same principles in your business.
I later used some of those principles in my own life.

One bold statement from this book which might trigger interest:

There are no bad teams, only bad leaders.

You be the judge :)

  1. (A) Crucial Conversations - Kerry Patterson

Helped me on all aspects of my life! I would strongly advise to give this one a spin.
It helps with handling those uncomfortable conversations.
I found it very useful in everyday life.

  1. (PG) Maximum Achievement: Strategies and Skills That Will Unlock Your Hidden Powers to Succeed - Brian Tracy

Planning and setting goals.
Of course there are more than just those two points, but they are my key focus points.
A bit long but worked my way thought it.

  1. (G) The 10X Rule: The Only Difference Between Success and Failure - Grant Cardone

Good motivation to get you stirred up to do more than just what is needed.
Aim for the moon and you’ll hit the trees type of thing.

  1. (A) The Like Switch - Jack Schafer PhD, Ph.D. Marvin Karlins Ph.D.

It’s from an ex-FBI agent sharing years of experience on attracting and winning people over.
I liked this one very much, it helped me to be a better person and to put others first.

  1. (PG) What Every BODY Is Saying - Joe Navarro, Marvin Karlins

Also from an ex-FBI agent sharing how to speed-reading people.
I can now recognise small things my kids do before they turn into little demons :)

  1. (PG) The Charisma Myth - Olivia Fox Cabane

How anyone can master the art and science of personal magnetism.
This one helped me with a few ideas on public speaking.
It also pushed me to sign up for my first public speaking event…even though it was just 5 minutes :)

  1. (PG) A Whole New Mind - Daniel H. Pink

Shared some science behind how people think.
Also on how to spark the creative side of the mind.
Good book, enjoyed it!

  1. (A) Drive - Daniel H. Pienk

The surprising truth about what motivates us.
As always, Dan Pienk likes to use science to explain all his findings.
This helped me on all aspects of life, work and family life.

  1. (A) Never Split the Difference: Negotiating as if Your Life Depended on It - Chris Voss, Tahl Raz

An ex-FBI agent sharing some principles when he was a hostage negotiator.
Fantastic book and I apply the principles daily at work and at home.
It totally changed my point of view when you ask a question and the answer is “no”.
The word “no”, is the beginning of the negotiation.

It also helped negotiating my kids into bed :)

We all negotiate everyday, you don’t have to like it, you just have to accept it.

Rating Description
(A) Awesome and enjoyed it a lot
(PG) Pretty good
(G) Good

Celebrations

Today I celebrate the completion of my personal goal I set 12 months ago.
If I can do it, so can you!
It does not have to be books, identify your own interest and commit to it for a year.
You will surprise yourself.

Final thought

I believe life is about relationships, and I strive to treat anyone with respect regardless their social status or background.
These books just highlighted my own believes again.

Use it…don’t use it :)

Netlify - Add Google Analytics to Hexo on Production-Deploy but Not on Branch-Deploy

How would you know if your website is seen or used? Google Analytics.

For a while I’ve been using Google Analytics to track my blog.
It’s free and it gives me insight on which blog post others find interesting too.
But…I’ve been doing it wrong.

Tools and Workflow

My blog framework is Hexo and I have Continuous Deployment working with the help of Netlify.
When I commit code to master branch, my blog will automatically deploy for your viewing pleasure.
On feature branches, which represents a new post, I configured Netlify’s Branch-Deploy to make my draft post available via a special URL.
I like the Branch-Deploy feature, as I can quickly share it with a friend to get feedback but more importantly, I can compare how the webpage renders on different devices.

My Netlify Build & Deploy => Continuous Deployment => Deploy context settings:

Deploy all branches pushed to Netlify

Adding Google Analytics (GA) to Hexo

Assuming you have a Universal Analytics tracking ID (UA number), adding GA to Hexo blog is very simple.
(If you don’t have a UA number, see resources below to create one)

Just add your Universal Analytics (UA) number to your theme’s ../themes/[YourThemeName]/_config.yml file,
and also add a new ../themes/[YourThemeName]/layout/_partial/google_analytics.ejs file to Hexo.

Add Google Analytics to Hexo

Here is the content of the google_analytics.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% if (theme.google_analytics){ %>
<!-- Google Analytics -->
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', '<%= theme.google_analytics %>', 'auto');
ga('send', 'pageview');

</script>
<!-- End Google Analytics -->
<% } %>

This works well and below is a sample of the Google Analytics code Hexo will generate in the head section of each page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
...
...
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-7445566-1"></script>
<script>
window.dataLayer = window.dataLayer || [];

function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-7445566-1');
</script>
<!-- Google Analytics End-->
</head>

My Mistake

After I’ve added GA to my blog, and I browsed the Branch-deploy URL (the test site), I realised GA recorded it as a real-time-active user.

I don’t want any stats recorded when I’m in Test/Draft, I’m only interested in Production stats…oops :)

My mission

How to get Hexo to generate Google Analytics (GA) on Production deployments only.

A quick hexo generate revealed when I remove UA number from the _config.yml, Hexo will remove all GA code from the pages too.

1
2
# Miscellaneous 
google_analytics:

I figured, if I can use Environment Variables to represent the Google Analytics UA tracking number, I would be able add the UA number to the _config.yml for Production deployments and remove it on Branch deployments.

My Solution

After a few minutes in the Netlify documentation, I discovered the awesome Stream Editor linux command sed.
This command will search and replace file content.

Now I have all the info to solve my issue.

Add GA_UA_PLACEHOLDER place holder text in the _config.yml which will be replaced on deployment.

1
2
# Miscellaneous 
google_analytics: GA_UA_PLACEHOLDER

Then, add the Environment Variables and sed (string replace) steps into the netlify.toml.

1
2
3
4
5
6
7
8
9
10
11
[build]
base = "blog"
publish = "blog/public"

[context.production]
environment = { GA_UA_PLACEHOLDER = "UA-7445566-1" }
command = "printenv && sed -i s/GA_UA_PLACEHOLDER/${GA_UA_PLACEHOLDER}/g ./themes/landscape/_config.yml && hexo generate && cp ../prod_headers.txt public/_headers --verbose"

[context.branch-deploy]
environment = { GA_UA_PLACEHOLDER = "" }
command = "printenv && sed -i s/GA_UA_PLACEHOLDER/${GA_UA_PLACEHOLDER}/g ./themes/landscape/_config.yml && hexo generate && cp ../branch_headers.txt public/_headers --verbose"

[context.production]: All steps under this section will execute when commit was detected on master branch.

environment = { GA_UA_PLACEHOLDER = "UA-7445566-1" }: Will set Environment Variable GA_UA_PLACEHOLDER to UA-7445566-1 when changes detected on master.

[context.branch-deploy]: All steps under this section will execute when commit was detected on any feature branch.

environment = { GA_UA_PLACEHOLDER = "" }: Will set Environment Variable GA_UA_PLACEHOLDER to empty string when changes detected on feature deployment.

printenv: Will list all the Environment Variables, just a debug thing I do :)

sed -i s/GA_UA_PLACEHOLDER/${GA_UA_PLACEHOLDER}/g ./themes/landscape/_config.yml: Will take _config.yml file as input and search for all occurrences of GA_UA_PLACEHOLDER and replace it with Environment Variable ${GA_UA_PLACEHOLDER}.

cp ../_headers.txt public/_headers: Will copy branch specific header file _header.txt to public folder with filename _headers, so that Netlify can apply the headers.

After these changes, the head section on each page for feature branches were GA free.
For master commits, the GA “magically” appeared.
Finally, my stats will reflect reality.

Use it…don’t use it :)

PS: Please feel free to leave a comment on how to improve this approach.

Google Analytics Resources

How to add a new website in Google Analytics
Google - Get started with Analytics