Unverified Commit f5149bcb authored by Dorian Johnson's avatar Dorian Johnson Committed by GitHub

feat: Analytics Tracking middleware (#739)

* Added middleware and typed it
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* Extracting Google Analytics tracker  util
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* Adds getGoogleAnalyticsConfig configuration helper and tests
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* config-types: add AnalyticsConfig
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* docs: add analytics to app config docs
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics: hacky method
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics: plugin architecture
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app config-custom: remove segment key

This key isn't secret, and is a dev environment anyhow, so do not need to
wipe from git history.
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app: remove old google analytics tag manager integration
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics: code style cleanup and PR feedback
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics: add typings for analytics event meta
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics typings: change "payload" to "properties"

"payload" already has meaning elsewhere in this context
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* app analytics: unknown instead of any in typings
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* jest config: reduce coverage req for ducks
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>

* Adds extra tracking to bookmarks, modifies mapping of event, adds google analytics and mixpanel dependencies
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Adding segment, updating docs
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Space
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* fe package: save package-lock.json
Signed-off-by: 's avatarDorian Johnson <2020@dorianj.net>
Co-authored-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>
parent fd970158
......@@ -19,7 +19,7 @@ module.exports = {
statements: 78, // 75
},
'./js/ducks': {
branches: 74, // 75
branches: 70, // 75
functions: 80,
lines: 80,
statements: 85,
......
......@@ -7,10 +7,8 @@ const configCustom: AppConfigCustom = {
curatedTags: [],
showAllTags: true,
},
google: {
enabled: false,
key: 'default-key',
sampleRate: 100,
analytics: {
plugins: [],
},
mailClientFeatures: {
feedbackEnabled: false,
......
......@@ -17,10 +17,8 @@ const configDefault: AppConfig = {
tableDescLength: 750,
columnDescLength: 250,
},
google: {
enabled: false,
key: 'default-key',
sampleRate: 100,
analytics: {
plugins: [],
},
indexDashboards: {
enabled: false,
......
......@@ -7,11 +7,11 @@ import { FilterType, ResourceType, SortCriteria } from '../interfaces';
*/
export interface AppConfig {
analytics: AnalyticsConfig;
badges: BadgeConfig;
browse: BrowseConfig;
date: DateFormatConfig;
editableText: EditableTextConfig;
google: GoogleAnalyticsConfig;
indexDashboards: IndexDashboardsConfig;
indexUsers: IndexUsersConfig;
userIdLabel?: string /* Temporary configuration due to lacking string customization/translation support */;
......@@ -27,11 +27,11 @@ export interface AppConfig {
}
export interface AppConfigCustom {
analytics?: AnalyticsConfig;
badges?: BadgeConfig;
browse?: BrowseConfig;
date?: DateFormatConfig;
editableText?: EditableTextConfig;
google?: GoogleAnalyticsConfig;
indexDashboards?: IndexDashboardsConfig;
indexUsers?: IndexUsersConfig;
userIdLabel?: string /* Temporary configuration due to lacking string customization/translation support */;
......@@ -47,15 +47,12 @@ export interface AppConfigCustom {
}
/**
* GoogleAnalyticsConfig - Customize 'gtag' - Google Tag Manager.
* AnalyticsConfig - Configure a single analytics destination
*
* Key - The unique analytics key for your site
* Sample Rate - The percentage of users (0 - 100) to track site speed.
* plugins - array of AnalyticsPlugin functions (upstream doesn't expose this type, so any).
*/
interface GoogleAnalyticsConfig {
enabled: boolean;
key: string;
sampleRate: number;
export interface AnalyticsConfig {
plugins: Array<any>;
}
/**
......@@ -319,6 +316,7 @@ export interface NumberStyleConfig {
style: NumberStyle;
config: string;
}
/**
* NumberFormatConfig - configurations for formatting different type of numbers like currency, percentage,number system
* this allows users to display numbers in desired format
......
......@@ -3,7 +3,7 @@ import { BadgeStyle, BadgeStyleConfig } from 'config/config-types';
import { TableMetadata } from 'interfaces/TableMetadata';
import { convertText, CaseType } from 'utils/textUtils';
import { FilterConfig, LinkConfig } from './config-types';
import { AnalyticsConfig, FilterConfig, LinkConfig } from './config-types';
import { ResourceType } from '../interfaces';
......@@ -76,6 +76,13 @@ export function getFilterConfigByResource(
return AppConfig.resourceConfig[resourceType].filterCategories;
}
/**
* Returns AnalyticsConfig.
*/
export function getAnalyticsConfig(): AnalyticsConfig {
return AppConfig.analytics;
}
/*
* Given a badge name, this will return a badge style and a display name.
* If these are not specified by config, it will default to some simple rules:
......
......@@ -78,6 +78,14 @@ describe('getFilterConfigByResource', () => {
});
});
describe('getAnalyticsConfig', () => {
it('returns the analytics configuration object', () => {
const expectedValue = AppConfig.analytics;
expect(ConfigUtils.getAnalyticsConfig()).toBe(expectedValue);
});
});
describe('getTableSortCriterias', () => {
it('returns the sorting criterias for tables', () => {
const expectedValue =
......
......@@ -25,6 +25,15 @@ export function addBookmark(
resourceKey,
resourceType,
},
meta: {
analytics: {
name: `${resourceType}/addBookmark`,
payload: {
category: 'Bookmark',
label: `${resourceKey}`,
},
},
},
type: AddBookmark.REQUEST,
};
}
......@@ -46,6 +55,15 @@ export function removeBookmark(
resourceKey,
resourceType,
},
meta: {
analytics: {
name: `${resourceType}/removeBookmark`,
payload: {
category: 'Bookmark',
label: `${resourceKey}`,
},
},
},
type: RemoveBookmark.REQUEST,
};
}
......
import { Bookmark, ResourceType, ResourceDict } from 'interfaces';
import {
AnalyticsEvent,
Bookmark,
ResourceType,
ResourceDict,
} from 'interfaces';
export enum AddBookmark {
REQUEST = 'amundsen/bookmark/ADD_REQUEST',
......@@ -11,6 +16,9 @@ export interface AddBookmarkRequest {
resourceKey: string;
resourceType: ResourceType;
};
meta: {
analytics: AnalyticsEvent;
};
}
export interface AddBookmarkResponse {
type: AddBookmark.SUCCESS | AddBookmark.FAILURE;
......@@ -30,6 +38,9 @@ export interface RemoveBookmarkRequest {
resourceKey: string;
resourceType: ResourceType;
};
meta: {
analytics: AnalyticsEvent;
};
}
export interface RemoveBookmarkResponse {
type: RemoveBookmark.SUCCESS | RemoveBookmark.FAILURE;
......
import { Middleware } from 'redux';
import { RootState } from '../rootReducer';
import { trackEvent } from '../../utils/analytics';
export const analyticsMiddleware: Middleware<
{}, // legacy type parameter added to satisfy interface signature
RootState
> = ({ getState }) => (next) => (action) => {
const result = next(action);
// Intercept actions with meta analytics
if (!action.meta || !action.meta.analytics) {
return result;
}
const { name, payload } = action.meta.analytics;
trackEvent(name, payload);
return result;
};
......@@ -32,7 +32,7 @@ export interface GlobalState {
user: UserReducerState;
}
export default combineReducers<GlobalState>({
const rootReducer = combineReducers<GlobalState>({
announcements,
bookmarks,
dashboard,
......@@ -46,3 +46,7 @@ export default combineReducers<GlobalState>({
tags,
user,
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
......@@ -12,6 +12,8 @@ import { createStore, applyMiddleware } from 'redux';
import { Router, Route, Switch } from 'react-router-dom';
import DocumentTitle from 'react-document-title';
import { analyticsMiddleware } from 'ducks/middlewares';
import { BrowserHistory } from 'utils/navigationUtils';
import DashboardPage from './pages/DashboardPage';
......@@ -33,6 +35,7 @@ import rootSaga from './ducks/rootSaga';
const sagaMiddleware = createSagaMiddleware();
const createStoreWithMiddleware = applyMiddleware(
ReduxPromise,
analyticsMiddleware,
sagaMiddleware
)(createStore);
const store = createStoreWithMiddleware(rootReducer);
......
export interface AnalyticsEvent {
name: string;
payload: { [prop: string]: unknown };
}
export * from './Analytics';
export * from './Announcements';
export * from './Badges';
export * from './Enums';
export * from './Feedback';
export * from './Issue';
export * from './Notifications';
export * from './Resources';
export * from './TableMetadata';
export * from './Tags';
export * from './User';
export * from './Issue';
export * from './Badges';
import Analytics, { AnalyticsInstance } from 'analytics';
import * as ConfigUtils from 'config/config-utils';
let sharedAnalyticsInstance;
export const analyticsInstance = (): AnalyticsInstance => {
if (sharedAnalyticsInstance) {
return sharedAnalyticsInstance;
}
const { plugins } = ConfigUtils.getAnalyticsConfig();
sharedAnalyticsInstance = Analytics({
app: 'amundsen',
version: '100',
plugins,
});
return sharedAnalyticsInstance;
};
export const trackEvent = (eventName: string, properties: Map<string, any>) => {
const analytics = analyticsInstance();
analytics.track(eventName, properties);
};
......@@ -129,10 +129,15 @@
"webworkify-webpack": "2.1.5"
},
"dependencies": {
"@analytics/google-analytics": "^0.5.2",
"@analytics/mixpanel": "^0.2.1",
"@analytics/segment": "^0.5.1",
"analytics": "^0.5.5",
"autosize": "^4.0.2",
"axios": "0.19.0",
"core-js": "^3.6.5",
"form-serialize": "^0.7.2",
"fsevents": "*",
"jquery": "^3.5.0",
"moment-timezone": "^0.5.31",
"path-browserify": "^1.0.1",
......
<!--
Copyright Contributors to the Amundsen project.
SPDX-License-Identifier: Apache-2.0
-->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= htmlWebpackPlugin.options.config.google.key%>"></script>
<script>
const google = <%= JSON.stringify(htmlWebpackPlugin.options.config.google) %>;
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', google.key);
gtag('create', google.key, { 'siteSpeedSampleRate': google.sampleRate });
</script>
<!--
Copyright Contributors to the Amundsen project.
SPDX-License-Identifier: Apache-2.0
-->
<script type="text/javascript">
// Feature detects Navigation Timing API support.
if (window.performance) {
// Gets the number of milliseconds since page load
// (and rounds the result since the value must be an integer).
const timeSincePageLoad = Math.round(performance.now());
// Sends the timing event to Google Analytics.
gtag('event', 'timing_complete', {
'name': 'load',
'value': timeSincePageLoad,
'event_category': 'JS Dependencies'
});
}
</script>
<!--
<!--
Copyright Contributors to the Amundsen project.
SPDX-License-Identifier: Apache-2.0
-->
......@@ -18,18 +18,13 @@
{% include 'fragments/icons-prod.html' %}
{% endif %}
{% if <%= htmlWebpackPlugin.options.config.google.enabled%> %}
{% include 'fragments/google-analytics-loader.html' %}
{% endif %}
<%= htmlWebpackPlugin.tags.headTags %>
<!-- Do not remove: some analytics scripts attach to this. -->
<script id="analyitcs-script-stub"></script>
</head>
<body class="{{ env }}">
<div id="content"/>
<%= htmlWebpackPlugin.tags.bodyTags %>
</body>
{% if <%= htmlWebpackPlugin.options.config.google.enabled%> %}
{% include 'fragments/google-analytics-post-loader.html' %}
{% endif %}
</html>
......@@ -37,9 +37,26 @@ This config allows you to specify various date formats across the app. There are
Reference for formatting: https://devhints.io/datetime#momentjs-format
## Google Analytics
## Analytics
_TODO: Please add doc_
Amundsen supports pluggable user behavior analytics via the [analytics](https://github.com/DavidWells/analytics) library.
To emit analytics to a given destination, you must use one of the provided plugins (open a PR if you need to install a different vendor), then specify it the config passing the configuration of your account. Multiple destinations are supported if you wish to emit to multiple backends simultaneously.
For example, to use Google analytics, you must add the import at the top of your `config-custom.ts` file: `import googleAnalytics from '@analytics/google-analytics';`, then add this config block:
```
analytics: {
plugins: [
googleAnalytics({
trackingId: '<YOUR_UA_CODE>',
sampleRate: 100
}),
],
}
```
We provide out of the box support for Mixpanel, Segment and Google Analytics. All [`@analytics/` plugins](https://github.com/DavidWells/analytics#analytic-plugins) are potentially supported, but you must first install the plugin: `npm install @analytics/<provider>` and send us a PR with it before you can use it.
## Indexing Optional Resources
......@@ -102,10 +119,12 @@ You can configure custom icons to be used throughout the UI when representing en
You can configure a specific display name to be used throughout the UI when representing entities from particular sources. On the `supportedSources` object, add an entry with the `id` used to reference that source and map to an object that specified the `displayName` for that source.
### Table Configuration
To configure Table related features we have created a new resource configuration `TableResourceConfig` which extends `BaseResourceConfig`. In addition to the configurations explained above it also supports `supportedDescriptionSources`.
#### Supported Description Sources
A table resource may have a source of table and column description attached to it. We can customize it by using `supportedDescriptionSources` object which is an optional object.
A table resource may have a source of table and column description attached to it. We can customize it by using `supportedDescriptionSources` object which is an optional object.
This object has `displayName` and `iconPath`, which can be used throughout the UI to represent a particular description source. See example in [config-default.ts](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/js/config/config-default.ts).
For configuring new description sources, add an entry in `supportedDescriptionSources` with the `id` used to reference that source and add desired display name and icon for it.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment