Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Add Google Analytics tracking if and only if a user has clicked "Accept" on the notification #409

Merged
merged 2 commits into from
Mar 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Google Analytics which activates only upon user's explicit consent [#409](https://github.com/open-apparel-registry/open-apparel-registry/pull/409)

### Changed

### Deprecated
Expand Down
1 change: 1 addition & 0 deletions src/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"google-map-react": "1.1.2",
"immutability-helper": "2.9.0",
"lodash": "4.17.11",
"moment": "2.24.0",
"prop-types": "15.6.2",
"react": "16.7.0",
"react-copy-to-clipboard": "5.0.1",
Expand Down
26 changes: 18 additions & 8 deletions src/app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
<meta http-equiv="pragma" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon/favicon-16x16.png">
Expand All @@ -32,15 +32,25 @@
<title>Open Apparel Registry (beta)</title>
<!-- Environment Variables -->
<script src="%PUBLIC_URL%/web/environment.js"></script>
<!-- TODO: Replace with actual Google Tag Manager snippet -->
<!-- Google Analytics -->
<script>
if (window.ENVIRONMENT &&
window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY &&
window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY !== "" &&
window.ENVIRONMENT.ENVIRONMENT !== 'development') {
console.log(window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY);
}
if (window.ENVIRONMENT &&
window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY &&
window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY !== "") {
// Initially disable tracking until we check that the user has accepted the notification
//
// See: https://developers.google.com/analytics/devguides/collection/analyticsjs/user-opt-out
//
// We will later delete this key and start Google Analytics tracking in the application code
// if it is verified that the user has selected "Accept" for the Google Analytics tracking
// notification. See: /src/app/src/util/util.ga.js
//
// Set for every environment to ensure it is always set.
var disableGAKey = 'ga-disable-' + window.ENVIRONMENT.REACT_APP_GOOGLE_ANALYTICS_KEY;
window[disableGAKey] = true;
}
</script>
<!-- End Google Analytics -->
<!--
Rollbar Configuration
https://docs.rollbar.com/docs/browser-js
Expand Down
91 changes: 91 additions & 0 deletions src/app/src/__tests__/util.ga.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-env jest */
/* eslint-disable object-shorthand */
/* eslint-disable func-names */
/* eslint-disable space-before-function-paren */

import {
userHasAcceptedOrRejectedGATracking,
userHasAcceptedGATracking,
userHasRejectedGATracking,
rejectGATracking,
acceptGATrackingAndStartTracking,
clearGATrackingDecision,
createGADisableKey,
} from '../util/util.ga.js';

beforeEach(() => {
const window = {};

window.localStorage = {
store: {},
getItem: function(key) {
return this.store[key] || null;
},
setItem: function(key, value) {
this.store[key] = value.toString();
},
removeItem: function(key) {
delete this.store[key];
},
clear: function() {
this.store = {};
},
};

window.ENVIRONMENT = {
REACT_APP_GOOGLE_ANALYTICS_KEY: 'GA_KEY',
};

global.window = window;
jest.spyOn(global.window.localStorage, 'setItem');
});

afterEach(() => {
kellyi marked this conversation as resolved.
Show resolved Hide resolved
window.localStorage = null;
});

it('creates a `ga-disable-ID` key for opting out of tracking', () => {
const expectedGADisableKey = 'ga-disable-test-key';
const testGAKey = 'test-key';

expect(createGADisableKey(testGAKey)).toBe(expectedGADisableKey);
});

it('correctly sets a `HAS_REJECTED_GA_TRACKING` value in localStorage', () => {
rejectGATracking();

expect(window.localStorage.setItem).toHaveBeenCalledTimes(2);
expect(window.localStorage.setItem).toHaveBeenCalledWith('GA_TRACKING', 'HAS_REJECTED_GA_TRACKING');
expect(window.localStorage.getItem('GA_TRACKING')).toBe('HAS_REJECTED_GA_TRACKING');

expect(userHasAcceptedOrRejectedGATracking()).toBe(true);
expect(userHasRejectedGATracking()).toBe(true);
expect(userHasAcceptedGATracking()).toBe(false);
});

it('correctly sets a `HAS_ACCEPTED_GA_TRACKING` value in localStorage', () => {
acceptGATrackingAndStartTracking();

expect(window.localStorage.setItem).toHaveBeenCalledTimes(2);
expect(window.localStorage.setItem).toHaveBeenCalledWith('GA_TRACKING', 'HAS_ACCEPTED_GA_TRACKING');
expect(window.localStorage.getItem('GA_TRACKING')).toBe('HAS_ACCEPTED_GA_TRACKING');

expect(userHasAcceptedOrRejectedGATracking()).toBe(true);
expect(userHasAcceptedGATracking()).toBe(true);
expect(userHasRejectedGATracking()).toBe(false);
});

it('correctly clears a `HAS_ACCEPTED_GA_TRACKING` decision value from localStorage', () => {
acceptGATrackingAndStartTracking();

expect(userHasAcceptedGATracking()).toBe(true);

jest.spyOn(window.localStorage, 'removeItem');
clearGATrackingDecision();

expect(window.localStorage.removeItem).toHaveBeenCalledTimes(1);
expect(window.localStorage.removeItem).toHaveBeenCalledWith('GA_TRACKING');

expect(userHasAcceptedGATracking()).toBe(false);
expect(userHasAcceptedOrRejectedGATracking()).toBe(false);
});
73 changes: 51 additions & 22 deletions src/app/src/components/GDPRNotification.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import React, { PureComponent } from 'react';
import React, { Component } from 'react';
import Snackbar from '@material-ui/core/Snackbar';
import Button from './Button';
import ShowOnly from './ShowOnly';
import COLOURS from '../util/COLOURS';

class GDPRNotification extends PureComponent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change from PureComponent to Component? (I'm not familiar with the subtle differences)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of PureComponent is that it has some characteristics which memoize it in certain circumstances, but I don't quite know whether that's true or what those circumstances are. In the interest of being able to reason about this a little more accurately I switched it out for a regular Component.

state = { open: true };
import {
userHasAcceptedOrRejectedGATracking,
userHasAcceptedGATracking,
acceptGATrackingAndStartTracking,
rejectGATracking,
startGATrackingIfUserHasAcceptedNotification,
} from '../util/util.ga';

export default class GDPRNotification extends Component {
state = { open: false };

componentDidMount() {
const dismissed = localStorage.getItem('dismissedGDPRAlert');
if (dismissed) {
this.setState({ open: false }); // eslint-disable-line react/no-did-mount-set-state
if (userHasAcceptedOrRejectedGATracking()) {
if (userHasAcceptedGATracking()) {
startGATrackingIfUserHasAcceptedNotification();
}

return null;
}

return this.setState(state => Object.assign({}, state, {
open: true,
}));
}

dismissGDPRAlert = () => {
this.setState({ open: false });
localStorage.setItem('dismissedGDPRAlert', true);
};
acceptGDPRAlertAndDismissSnackbar = () => this.setState(
state => Object.assign({}, state, { open: false }),
acceptGATrackingAndStartTracking,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent function name

);

rejectGDPRAlertAndDismissSnackbar = () => this.setState(
state => Object.assign({}, state, { open: false }),
rejectGATracking,
);

render() {
const GDPRActions = (
Expand All @@ -28,19 +48,30 @@ class GDPRNotification extends PureComponent {
background: COLOURS.LIGHT_BLUE,
marginRight: '10px',
}}
onClick={this.dismissGDPRAlert}
onClick={this.rejectGDPRAlertAndDismissSnackbar}
/>
<Button
text="Accept"
onClick={this.acceptGDPRAlertAndDismissSnackbar}
/>
<Button text="Accept" onClick={this.dismissGDPRAlert} />
</div>
);

const GDPRMessage =
`We use cookies to collect and analyze
information on site performance and usage,
and to enhance content. By clicking Accept,
you agree to allow cookies to be placed.
To find out more, visit our terms of service
and our privacy policy.`;
const snackbarMessage = (
<div>
The Open Apparel Registry uses cookies to collect and analyze
site performance and usage. By clicking the Accept button, you
agree to allow us to place cookies and share information with
Google Analytics. For more information, please visit our{' '}
<a
href="https://info.openapparel.org/tos/"
target="_blank"
rel="noopener noreferrer"
>
Terms and Conditions of Use and Privacy Policy.
</a>
</div>
);

return (
<ShowOnly when={this.state.open}>
Expand All @@ -52,11 +83,9 @@ class GDPRNotification extends PureComponent {
horizontal: 'right',
}}
action={GDPRActions}
message={GDPRMessage}
message={snackbarMessage}
/>
</ShowOnly>
);
}
}

export default GDPRNotification;