Skip to content
Snippets Groups Projects
Unverified Commit 5de1cde7 authored by Kyle Doherty's avatar Kyle Doherty Committed by GitHub
Browse files

track events via redux action dispatch (#6977)

* POC track events via redux action dispatch

* update and document track action

* add tests
parent 4eafe4cf
Branches
Tags
No related merge requests found
......@@ -3,6 +3,7 @@
import { combineReducers, applyMiddleware, createStore, compose } from "redux";
import { reducer as form } from "redux-form";
import { routerReducer as routing, routerMiddleware } from "react-router-redux";
import MetabaseAnalytics from "metabase/lib/analytics";
import promise from "redux-promise";
import logger from "redux-logger";
......@@ -28,6 +29,73 @@ const devToolsExtension = window.devToolsExtension
? window.devToolsExtension()
: f => f;
// Look for redux action names that take the form `metabase/<app_section>/<ACTION_NAME>
const METABASE_TRACKABLE_ACTION_REGEX = /^metabase\/(.+)\/([^\/]+)$/;
/**
* Track events by looking at redux dispatch
* -----
* This redux middleware is meant to help automate event capture for instances
* that opt in to anonymous tracking by looking at redux actions and either
* using the name of the action, or defined analytics metadata to send event
* data to GA. This makes it un-necessary to instrument individual redux actions
*
* Any actions with a name takes the form `metabase/.../...` will be automatially captured
*
* Ignoring actions:
* Any actions we want to ignore can be bypassed by including a meta object with ignore: true
* {
* type: "...",
* meta: {
* analytics: { ignore: true }
* }
* }
*
* Customizing event names:
* If we don't want to use the action name metadata can be added to the action
* to customize the name
*
* {
* type: "...",
* meta: {
* analytics: {
* category: "foo",
* action: "bar",
* label: "baz",
* value: "qux"
* }
* }
*}
*/
export const trackEvent = ({ dispatch, getState }) => next => action => {
// look for the meta analytics object if it exists, this gets used to
// do customization of the event identifiers sent to GA
const analytics = action.meta && action.meta.analytics;
if (analytics) {
if (!analytics.ignore) {
MetabaseAnalytics.trackEvent(
analytics.category,
analytics.action,
analytics.label,
analytics.value,
);
}
} else if (METABASE_TRACKABLE_ACTION_REGEX.test(action.type)) {
// if there is no analytics metadata on the action, look to see if it's
// an action name we want to track based on the format of the aciton name
// eslint doesn't like the _ to ignore the first bit
// eslint-disable-next-line
const [_, categoryName, actionName] = action.type.match(
METABASE_TRACKABLE_ACTION_REGEX,
);
MetabaseAnalytics.trackEvent(categoryName, actionName);
}
return next(action);
};
export function getStore(reducers, history, intialState, enhancer = a => a) {
const reducer = combineReducers({
...reducers,
......@@ -37,6 +105,7 @@ export function getStore(reducers, history, intialState, enhancer = a => a) {
const middleware = [
thunkWithDispatchAction,
trackEvent,
promise,
...(DEBUG ? [logger] : []),
...(history ? [routerMiddleware(history)] : []),
......
import { trackEvent } from "metabase/store";
import MetabaseAnalytics from "metabase/lib/analytics";
jest.mock("metabase/lib/analytics", () => ({
trackEvent: jest.fn(),
}));
// fake next for redux
const next = jest.fn();
describe("store", () => {
describe("trackEvent", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("should call MetabaseAnalytics with the proper custom values", () => {
const testAction = {
type: "metabase/test/ACTION_NAME",
meta: {
analytics: {
category: "cool",
action: "action",
label: "labeled",
value: "value",
},
},
};
trackEvent({})(next)(testAction);
expect(MetabaseAnalytics.trackEvent).toHaveBeenCalledTimes(1);
expect(MetabaseAnalytics.trackEvent).toHaveBeenCalledWith(
"cool",
"action",
"labeled",
"value",
);
});
it("should ignore actions if ignore is true", () => {
const testAction = {
type: "metabase/test/ACTION_NAME",
meta: {
analytics: {
ignore: true,
},
},
};
trackEvent({})(next)(testAction);
expect(MetabaseAnalytics.trackEvent).toHaveBeenCalledTimes(0);
});
it("should use the action name if no analytics action is present", () => {
const testAction = {
type: "metabase/test/ACTION_NAME",
};
trackEvent({})(next)(testAction);
expect(MetabaseAnalytics.trackEvent).toHaveBeenCalledWith(
"test",
"ACTION_NAME",
);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment