From f351459b680e87c1fe05bbc684a9646cd8172efd Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Fri, 22 Oct 2021 19:47:52 +0300
Subject: [PATCH] Add snowplow tracking (#18293)

---
 .../auth/components/SSOButton.jsx             |   4 +-
 .../src/metabase/admin/databases/database.js  |  28 ++--
 .../datamodel/components/FieldRemapping.jsx   |  18 ++-
 .../components/database/ColumnItem.jsx        |   8 +-
 .../containers/MetadataEditorApp.jsx          |   4 +-
 .../admin/datamodel/containers/MetricApp.jsx  |   6 +-
 .../admin/datamodel/containers/SegmentApp.jsx |   6 +-
 .../src/metabase/admin/datamodel/field.js     |   6 +-
 .../src/metabase/admin/datamodel/table.js     |   6 +-
 .../admin/people/components/GroupsListing.jsx |   8 +-
 frontend/src/metabase/admin/people/people.js  |   6 +-
 .../metabase/admin/permissions/permissions.js |  14 +-
 .../settings/components/SettingsEmailForm.jsx |  14 +-
 .../settings/components/SettingsSlackForm.jsx |  20 ++-
 .../components/widgets/EmbeddingLegalese.jsx  |   4 +-
 .../components/widgets/PublicLinksListing.jsx |   4 +-
 .../settings/containers/SettingsEditorApp.jsx |   6 +-
 frontend/src/metabase/app.js                  |  20 +--
 frontend/src/metabase/auth/auth.js            |   8 +-
 .../auth/containers/PasswordResetApp.jsx      |   4 +-
 .../AddSeriesModal/AddSeriesModal.jsx         |  16 +-
 .../AddSeriesModal/QuestionList.jsx           |   8 +-
 .../dashboard/components/DashboardGrid.jsx    |   4 +-
 .../components/DashboardSidebars.jsx          |   4 +-
 .../components/RemoveFromDashboardModal.jsx   |   4 +-
 .../containers/AutomaticDashboardApp.jsx      |   9 +-
 .../DashboardSharingEmbeddingModal.jsx        |   4 +-
 .../dashboard/hoc/DashboardControls.jsx       |   4 +-
 frontend/src/metabase/entities/users.js       |  15 +-
 frontend/src/metabase/lib/CODENOTIFY          |   3 +
 frontend/src/metabase/lib/analytics.js        | 150 +++++++++++-------
 frontend/src/metabase/lib/redux.js            |   4 +-
 .../components/widgets/EmbedModalContent.jsx  |   7 +-
 .../public/components/widgets/SharingPane.jsx |   6 +-
 .../metabase/pulse/components/PulseEdit.jsx   |  10 +-
 .../pulse/components/PulseEditCards.jsx       |   4 +-
 .../pulse/components/PulseEditChannels.jsx    |   8 +-
 .../pulse/components/RecipientPicker.jsx      |   4 +-
 .../src/metabase/query_builder/actions.js     |  25 +--
 .../query_builder/components/AlertModals.jsx  |  10 +-
 .../components/ExtendedOptions.jsx            |  10 +-
 .../components/filters/FilterOptions.jsx      |   9 +-
 .../template_tags/TagEditorSidebar.jsx        |   4 +-
 .../containers/QuestionEmbedWidget.jsx        |   4 +-
 frontend/src/metabase/redux/undo.js           |   6 +-
 .../metrics/MetricDetailContainer.jsx         |   4 +-
 frontend/src/metabase/reference/reference.js  |   6 +-
 frontend/src/metabase/setup/actions.js        |   4 +-
 .../components/DatabaseConnectionStep.jsx     |  14 +-
 .../components/DatabaseSchedulingStep.jsx     |   8 +-
 .../setup/components/PreferencesStep.jsx      |   4 +-
 .../src/metabase/setup/components/Setup.jsx   |   6 +-
 .../metabase/setup/components/UserStep.jsx    |  10 +-
 .../components/CardRenderer.jsx               |   7 +-
 .../components/ChartClickActions.jsx          |  17 +-
 .../components/ChartSettings.jsx              |   4 +-
 .../components/Visualization.jsx              |   4 +-
 .../settings/ChartSettingsTableFormatting.jsx |   8 +-
 .../metabase/visualizations/lib/settings.js   |   4 +-
 frontend/test/__support__/mocks.js            |   1 +
 package.json                                  |   1 +
 .../inline_js/index_ganalytics.js             |   9 --
 src/metabase/server/middleware/security.clj   |   3 +
 yarn.lock                                     |  50 ++++++
 64 files changed, 424 insertions(+), 266 deletions(-)
 create mode 100644 frontend/src/metabase/lib/CODENOTIFY

diff --git a/enterprise/frontend/src/metabase-enterprise/auth/components/SSOButton.jsx b/enterprise/frontend/src/metabase-enterprise/auth/components/SSOButton.jsx
index 400a6140784..70da9969f58 100644
--- a/enterprise/frontend/src/metabase-enterprise/auth/components/SSOButton.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/auth/components/SSOButton.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 
 import { IFRAMED } from "metabase/lib/dom";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import AuthProviderButton from "metabase/auth/components/AuthProviderButton";
@@ -17,7 +17,7 @@ export default class SSOButton extends React.Component {
 
   handleClick = () => {
     const { redirect } = this.props.location.query;
-    MetabaseAnalytics.trackEvent("Auth", "SSO Login Start");
+    MetabaseAnalytics.trackStructEvent("Auth", "SSO Login Start");
     // use `window.location` instead of `push` since it's not a frontend route
     window.location =
       MetabaseSettings.get("site-url") +
diff --git a/frontend/src/metabase/admin/databases/database.js b/frontend/src/metabase/admin/databases/database.js
index 72a4d9e1c11..296e68a0cfc 100644
--- a/frontend/src/metabase/admin/databases/database.js
+++ b/frontend/src/metabase/admin/databases/database.js
@@ -6,7 +6,7 @@ import {
 } from "metabase/lib/redux";
 import { push } from "react-router-redux";
 import { t } from "ttag";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import { MetabaseApi } from "metabase/services";
@@ -151,7 +151,7 @@ export const addSampleDataset = createThunkAction(
             reload: true,
           }),
         );
-        MetabaseAnalytics.trackEvent("Databases", "Add Sample Data");
+        MetabaseAnalytics.trackStructEvent("Databases", "Add Sample Data");
         return sampleDataset;
       } catch (error) {
         console.error("error adding sample dataset", error);
@@ -201,13 +201,17 @@ export const createDatabase = function(database) {
       dispatch.action(CREATE_DATABASE_STARTED, {});
       const action = await dispatch(Databases.actions.create(database));
       const createdDatabase = Databases.HACK_getObjectFromAction(action);
-      MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);
+      MetabaseAnalytics.trackStructEvent(
+        "Databases",
+        "Create",
+        database.engine,
+      );
 
       dispatch.action(CREATE_DATABASE);
       dispatch(push("/admin/databases?created=" + createdDatabase.id));
     } catch (error) {
       console.error("error creating a database", error);
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Databases",
         "Create Failed",
         database.engine,
@@ -223,11 +227,15 @@ export const updateDatabase = function(database) {
       dispatch.action(UPDATE_DATABASE_STARTED, { database });
       const action = await dispatch(Databases.actions.update(database));
       const savedDatabase = Databases.HACK_getObjectFromAction(action);
-      MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);
+      MetabaseAnalytics.trackStructEvent(
+        "Databases",
+        "Update",
+        database.engine,
+      );
 
       dispatch.action(UPDATE_DATABASE, { database: savedDatabase });
     } catch (error) {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Databases",
         "Update Failed",
         database.engine,
@@ -257,7 +265,7 @@ export const deleteDatabase = function(databaseId, isDetailView = true) {
       dispatch.action(DELETE_DATABASE_STARTED, { databaseId });
       dispatch(push("/admin/databases/"));
       await dispatch(Databases.actions.delete({ id: databaseId }));
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Databases",
         "Delete",
         isDetailView ? "Using Detail" : "Using List",
@@ -277,7 +285,7 @@ export const syncDatabaseSchema = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.db_sync_schema({ dbId: databaseId });
-        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        MetabaseAnalytics.trackStructEvent("Databases", "Manual Sync");
         return call;
       } catch (error) {
         console.log("error syncing database", error);
@@ -293,7 +301,7 @@ export const rescanDatabaseFields = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.db_rescan_values({ dbId: databaseId });
-        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        MetabaseAnalytics.trackStructEvent("Databases", "Manual Sync");
         return call;
       } catch (error) {
         console.log("error syncing database", error);
@@ -309,7 +317,7 @@ export const discardSavedFieldValues = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.db_discard_values({ dbId: databaseId });
-        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        MetabaseAnalytics.trackStructEvent("Databases", "Manual Sync");
         return call;
       } catch (error) {
         console.log("error syncing database", error);
diff --git a/frontend/src/metabase/admin/datamodel/components/FieldRemapping.jsx b/frontend/src/metabase/admin/datamodel/components/FieldRemapping.jsx
index edbcca87c83..dfee9af9ff0 100644
--- a/frontend/src/metabase/admin/datamodel/components/FieldRemapping.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FieldRemapping.jsx
@@ -15,7 +15,7 @@ import ButtonWithStatus from "metabase/components/ButtonWithStatus";
 
 import SelectSeparator from "../components/SelectSeparator";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import Dimension, { FieldDimension } from "metabase-lib/lib/Dimension";
 import Question from "metabase-lib/lib/Question";
@@ -113,7 +113,7 @@ export default class FieldRemapping extends React.Component {
     this.clearEditingStates();
 
     if (mappingType.type === "original") {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Data Model",
         "Change Remapping Type",
         "No Remapping",
@@ -125,7 +125,7 @@ export default class FieldRemapping extends React.Component {
       const entityNameFieldId = this.getFKTargetTableEntityNameOrNull();
 
       if (entityNameFieldId) {
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Data Model",
           "Change Remapping Type",
           "Foreign Key",
@@ -146,7 +146,7 @@ export default class FieldRemapping extends React.Component {
         });
       }
     } else if (mappingType.type === "custom") {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Data Model",
         "Change Remapping Type",
         "Custom Remappings",
@@ -182,7 +182,10 @@ export default class FieldRemapping extends React.Component {
     // TODO Atte Keinänen 7/10/17: Use Dimension class when migrating to metabase-lib
     const dimension = Dimension.parseMBQL(foreignKeyClause);
     if (dimension && dimension instanceof FieldDimension && dimension.fk()) {
-      MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
+      MetabaseAnalytics.trackStructEvent(
+        "Data Model",
+        "Update FK Remapping Target",
+      );
       await updateFieldDimension(
         { id: field.id },
         {
@@ -390,7 +393,10 @@ export class ValueRemappings extends React.Component {
   }
 
   onSaveClick = () => {
-    MetabaseAnalytics.trackEvent("Data Model", "Update Custom Remappings");
+    MetabaseAnalytics.trackStructEvent(
+      "Data Model",
+      "Update Custom Remappings",
+    );
     // Returns the promise so that ButtonWithStatus can show the saving status
     return this.props.updateRemappings(this.state.editingRemappings);
   };
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
index 8180734d084..7bfcd8eed4b 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
@@ -18,7 +18,7 @@ import _ from "underscore";
 import cx from "classnames";
 
 import type { Field } from "metabase-types/types/Field";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 @withRouter
 export default class Column extends Component {
@@ -152,7 +152,7 @@ export class SemanticTypeAndTargetPicker extends Component {
       await updateField({ semantic_type });
     }
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "Data Model",
       "Update Field Special-Type",
       semantic_type,
@@ -167,7 +167,7 @@ export class SemanticTypeAndTargetPicker extends Component {
         currency,
       },
     });
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "Data Model",
       "Update Currency Type",
       currency,
@@ -176,7 +176,7 @@ export class SemanticTypeAndTargetPicker extends Component {
 
   handleChangeTarget = async ({ target: { value: fk_target_field_id } }) => {
     await this.props.updateField({ fk_target_field_id });
-    MetabaseAnalytics.trackEvent("Data Model", "Update Field Target");
+    MetabaseAnalytics.trackStructEvent("Data Model", "Update Field Target");
   };
 
   render() {
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
index 4d93baede74..296e992d5ad 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
@@ -4,7 +4,7 @@ import { connect } from "react-redux";
 import { push, replace } from "react-router-redux";
 
 import { t } from "ttag";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import AdminEmptyText from "metabase/components/AdminEmptyText";
 import MetadataHeader from "../components/database/MetadataHeader";
@@ -73,7 +73,7 @@ class MetadataEditor extends Component {
 
   toggleShowSchema() {
     this.setState({ isShowingSchema: !this.state.isShowingSchema });
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "Data Model",
       "Show OG Schema",
       !this.state.isShowingSchema,
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
index a982ff24619..60cccd99d3f 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import Metrics from "metabase/entities/metrics";
 
 import { updatePreviewSummary } from "../datamodel";
@@ -25,7 +25,7 @@ const mapStateToProps = (state, props) => ({
 class UpdateMetricForm extends Component {
   onSubmit = async metric => {
     await this.props.updateMetric(metric);
-    MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
+    MetabaseAnalytics.trackStructEvent("Data Model", "Metric Updated");
     this.props.onChangeLocation(`/admin/datamodel/metrics`);
   };
 
@@ -47,7 +47,7 @@ class CreateMetricForm extends Component {
       ...metric,
       table_id: metric.definition["source-table"],
     });
-    MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
+    MetabaseAnalytics.trackStructEvent("Data Model", "Metric Updated");
     this.props.onChangeLocation(`/admin/datamodel/metrics`);
   };
 
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
index 88218b75b5c..e601eb2e72e 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import Segments from "metabase/entities/segments";
 
 import { updatePreviewSummary } from "../datamodel";
@@ -25,7 +25,7 @@ const mapStateToProps = (state, props) => ({
 class UpdateSegmentForm extends Component {
   onSubmit = async segment => {
     await this.props.updateSegment(segment);
-    MetabaseAnalytics.trackEvent("Data Model", "Segment Updated");
+    MetabaseAnalytics.trackStructEvent("Data Model", "Segment Updated");
     this.props.onChangeLocation(`/admin/datamodel/segments`);
   };
 
@@ -47,7 +47,7 @@ class CreateSegmentForm extends Component {
       ...segment,
       table_id: segment.definition["source-table"],
     });
-    MetabaseAnalytics.trackEvent("Data Model", "Segment Updated");
+    MetabaseAnalytics.trackStructEvent("Data Model", "Segment Updated");
     this.props.onChangeLocation(`/admin/datamodel/segments`);
   };
 
diff --git a/frontend/src/metabase/admin/datamodel/field.js b/frontend/src/metabase/admin/datamodel/field.js
index 99d65b71bae..d1748c2c2df 100644
--- a/frontend/src/metabase/admin/datamodel/field.js
+++ b/frontend/src/metabase/admin/datamodel/field.js
@@ -1,6 +1,6 @@
 import { createThunkAction } from "metabase/lib/redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { MetabaseApi } from "metabase/services";
 
 export const RESCAN_FIELD_VALUES = "metabase/admin/fields/RESCAN_FIELD_VALUES";
@@ -13,7 +13,7 @@ export const rescanFieldValues = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.field_rescan_values({ fieldId });
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Data Model",
           "Manual Re-scan Field Values",
         );
@@ -31,7 +31,7 @@ export const discardFieldValues = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.field_discard_values({ fieldId });
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Data Model",
           "Manual Discard Field Values",
         );
diff --git a/frontend/src/metabase/admin/datamodel/table.js b/frontend/src/metabase/admin/datamodel/table.js
index 16174df80fc..c327319e075 100644
--- a/frontend/src/metabase/admin/datamodel/table.js
+++ b/frontend/src/metabase/admin/datamodel/table.js
@@ -1,6 +1,6 @@
 import { createThunkAction } from "metabase/lib/redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { MetabaseApi } from "metabase/services";
 
 export const RESCAN_TABLE_VALUES = "metabase/admin/tables/RESCAN_TABLE_VALUES";
@@ -13,7 +13,7 @@ export const rescanTableFieldValues = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.table_rescan_values({ tableId });
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Data Model",
           "Manual Re-scan Field Values for Table",
         );
@@ -31,7 +31,7 @@ export const discardTableFieldValues = createThunkAction(
     return async function(dispatch, getState) {
       try {
         const call = await MetabaseApi.table_discard_values({ tableId });
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Data Model",
           "Manual Discard Field Values for Table",
         );
diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.jsx b/frontend/src/metabase/admin/people/components/GroupsListing.jsx
index 188a55cdf73..dbeae7f2af1 100644
--- a/frontend/src/metabase/admin/people/components/GroupsListing.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupsListing.jsx
@@ -5,7 +5,7 @@ import { Link } from "react-router";
 import _ from "underscore";
 import cx from "classnames";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import {
   isDefaultGroup,
   isAdminGroup,
@@ -268,7 +268,7 @@ export default class GroupsListing extends Component {
 
   // TODO: move this to Redux
   async onAddGroupCreateButtonClicked() {
-    MetabaseAnalytics.trackEvent("People Groups", "Group Added");
+    MetabaseAnalytics.trackStructEvent("People Groups", "Group Added");
 
     try {
       await this.props.create({ name: this.state.text });
@@ -329,7 +329,7 @@ export default class GroupsListing extends Component {
       this.setState({ groupBeingEdited: null });
     } else {
       // ok, fire off API call to change the group
-      MetabaseAnalytics.trackEvent("People Groups", "Group Updated");
+      MetabaseAnalytics.trackStructEvent("People Groups", "Group Updated");
       try {
         await this.props.update({ id: group.id, name: group.name });
         this.setState({ groupBeingEdited: null });
@@ -344,7 +344,7 @@ export default class GroupsListing extends Component {
 
   // TODO: move this to Redux
   async onDeleteGroupClicked(group) {
-    MetabaseAnalytics.trackEvent("People Groups", "Group Deleted");
+    MetabaseAnalytics.trackStructEvent("People Groups", "Group Deleted");
     try {
       await this.props.delete(group);
     } catch (error) {
diff --git a/frontend/src/metabase/admin/people/people.js b/frontend/src/metabase/admin/people/people.js
index f1f6b09ad7d..a772ceb7ac5 100644
--- a/frontend/src/metabase/admin/people/people.js
+++ b/frontend/src/metabase/admin/people/people.js
@@ -4,7 +4,7 @@ import {
   combineReducers,
 } from "metabase/lib/redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { PermissionsApi } from "metabase/services";
 
@@ -38,7 +38,7 @@ export const createMembership = createAction(
       user_id: userId,
       group_id: groupId,
     });
-    MetabaseAnalytics.trackEvent("People Groups", "Membership Added");
+    MetabaseAnalytics.trackStructEvent("People Groups", "Membership Added");
     return {
       user_id: userId,
       group_id: groupId,
@@ -51,7 +51,7 @@ export const deleteMembership = createAction(
   DELETE_MEMBERSHIP,
   async ({ membershipId }) => {
     await PermissionsApi.deleteMembership({ id: membershipId });
-    MetabaseAnalytics.trackEvent("People Groups", "Membership Deleted");
+    MetabaseAnalytics.trackStructEvent("People Groups", "Membership Deleted");
     return membershipId;
   },
 );
diff --git a/frontend/src/metabase/admin/permissions/permissions.js b/frontend/src/metabase/admin/permissions/permissions.js
index 6870696dac8..3ab1833a5a5 100644
--- a/frontend/src/metabase/admin/permissions/permissions.js
+++ b/frontend/src/metabase/admin/permissions/permissions.js
@@ -11,7 +11,7 @@ import {
 import { CollectionsApi, PermissionsApi } from "metabase/services";
 import Group from "metabase/entities/groups";
 import Tables from "metabase/entities/tables";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import {
   inferAndUpdateEntityPermissions,
   updateFieldsPermission,
@@ -122,7 +122,7 @@ const SAVE_DATA_PERMISSIONS =
 export const saveDataPermissions = createThunkAction(
   SAVE_DATA_PERMISSIONS,
   () => async (_dispatch, getState) => {
-    MetabaseAnalytics.trackEvent("Permissions", "save");
+    MetabaseAnalytics.trackStructEvent("Permissions", "save");
     const {
       dataPermissions,
       dataPermissionsRevision,
@@ -147,7 +147,7 @@ const SAVE_COLLECTION_PERMISSIONS =
 export const saveCollectionPermissions = createThunkAction(
   SAVE_COLLECTION_PERMISSIONS,
   namespace => async (_dispatch, getState) => {
-    MetabaseAnalytics.trackEvent("Permissions", "save");
+    MetabaseAnalytics.trackStructEvent("Permissions", "save");
     const {
       collectionPermissions,
       collectionPermissionsRevision,
@@ -209,7 +209,7 @@ const dataPermissions = handleActions(
         const { value, groupId, entityId, metadata, permission } = payload;
 
         if (entityId.tableId != null) {
-          MetabaseAnalytics.trackEvent("Permissions", "fields", value);
+          MetabaseAnalytics.trackStructEvent("Permissions", "fields", value);
           const updatedPermissions = updateFieldsPermission(
             state,
             groupId,
@@ -224,7 +224,7 @@ const dataPermissions = handleActions(
             metadata,
           );
         } else if (entityId.schemaName != null) {
-          MetabaseAnalytics.trackEvent("Permissions", "tables", value);
+          MetabaseAnalytics.trackStructEvent("Permissions", "tables", value);
           return updateTablesPermission(
             state,
             groupId,
@@ -233,7 +233,7 @@ const dataPermissions = handleActions(
             metadata,
           );
         } else if (permission.name === "native") {
-          MetabaseAnalytics.trackEvent("Permissions", "native", value);
+          MetabaseAnalytics.trackStructEvent("Permissions", "native", value);
           return updateNativePermission(
             state,
             groupId,
@@ -242,7 +242,7 @@ const dataPermissions = handleActions(
             metadata,
           );
         } else {
-          MetabaseAnalytics.trackEvent("Permissions", "schemas", value);
+          MetabaseAnalytics.trackStructEvent("Permissions", "schemas", value);
           return updateSchemasPermission(
             state,
             groupId,
diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
index 37a3bbb539d..99a5b5afe69 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
@@ -9,7 +9,7 @@ import MarginHostingCTA from "metabase/admin/settings/components/widgets/MarginH
 
 import SettingsBatchForm from "./SettingsBatchForm";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import {
@@ -56,12 +56,20 @@ export default class SettingsEmailForm extends Component {
     try {
       await this.props.sendTestEmail();
       this.setState({ sendingEmail: "success" });
-      MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "success");
+      MetabaseAnalytics.trackStructEvent(
+        "Email Settings",
+        "Test Email",
+        "success",
+      );
 
       // show a confirmation for 3 seconds, then return to normal
       setTimeout(() => this.setState({ sendingEmail: "default" }), 3000);
     } catch (error) {
-      MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "error");
+      MetabaseAnalytics.trackStructEvent(
+        "Email Settings",
+        "Test Email",
+        "error",
+      );
       this.setState({ sendingEmail: "default" });
       // NOTE: reaching into form component is not ideal
       this._form.setFormErrors(this._form.handleFormErrors(error));
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
index 490b4617c6d..30e66eb253b 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import PropTypes from "prop-types";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseUtils from "metabase/lib/utils";
 import SettingsSetting from "./SettingsSetting";
 import { updateSlackSettings } from "../settings";
@@ -126,7 +126,11 @@ export default class SettingsSlackForm extends Component {
     });
 
     if (element.key === "metabot-enabled") {
-      MetabaseAnalytics.trackEvent("Slack Settings", "Toggle Metabot", value);
+      MetabaseAnalytics.trackStructEvent(
+        "Slack Settings",
+        "Toggle Metabot",
+        value,
+      );
     }
   }
 
@@ -163,7 +167,11 @@ export default class SettingsSlackForm extends Component {
             submitting: "success",
           });
 
-          MetabaseAnalytics.trackEvent("Slack Settings", "Update", "success");
+          MetabaseAnalytics.trackStructEvent(
+            "Slack Settings",
+            "Update",
+            "success",
+          );
 
           // show a confirmation for 3 seconds, then return to normal
           setTimeout(() => this.setState({ submitting: "default" }), 3000);
@@ -174,7 +182,11 @@ export default class SettingsSlackForm extends Component {
             formErrors: this.handleFormErrors(error),
           });
 
-          MetabaseAnalytics.trackEvent("Slack Settings", "Update", "error");
+          MetabaseAnalytics.trackStructEvent(
+            "Slack Settings",
+            "Update",
+            "error",
+          );
         },
       );
     }
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
index d77f8ef4af0..82f5aceaf24 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
@@ -1,6 +1,6 @@
 /* eslint-disable react/prop-types */
 import React from "react";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { t } from "ttag";
 import ExternalLink from "metabase/components/ExternalLink";
 
@@ -24,7 +24,7 @@ const EmbeddingLegalese = ({ onChange }) => (
       <button
         className="Button Button--primary"
         onClick={() => {
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "Admin Embed Settings",
             "Embedding Enable Click",
           );
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 5368228bd0d..a5a7ca271da 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -9,7 +9,7 @@ import { t } from "ttag";
 import { CardApi, DashboardApi } from "metabase/services";
 import * as Urls from "metabase/lib/urls";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 type PublicLink = {
   id: string,
@@ -69,7 +69,7 @@ export default class PublicLinksListing extends Component {
   }
 
   trackEvent(label: string) {
-    MetabaseAnalytics.trackEvent(`Admin ${this.props.type}`, label);
+    MetabaseAnalytics.trackStructEvent(`Admin ${this.props.type}`, label);
   }
 
   render() {
diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
index ac18a2408b2..e0187897602 100644
--- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
+++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
@@ -6,7 +6,7 @@ import { connect } from "react-redux";
 import { t } from "ttag";
 
 import title from "metabase/hoc/Title";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import AdminLayout from "metabase/components/AdminLayout";
 import { NotFound } from "metabase/containers/ErrorPages";
@@ -94,7 +94,7 @@ export default class SettingsEditorApp extends Component {
 
       const value = prepareAnalyticsValue(setting);
 
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "General Settings",
         setting.display_name || setting.key,
         value,
@@ -105,7 +105,7 @@ export default class SettingsEditorApp extends Component {
       const message =
         error && (error.message || (error.data && error.data.message));
       this.saveStatusRef.current.setSaveError(message);
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "General Settings",
         setting.display_name,
         "error",
diff --git a/frontend/src/metabase/app.js b/frontend/src/metabase/app.js
index d665a75024c..ed77382dbc3 100644
--- a/frontend/src/metabase/app.js
+++ b/frontend/src/metabase/app.js
@@ -33,9 +33,7 @@ import ReactDOM from "react-dom";
 import { Provider } from "react-redux";
 import { ThemeProvider } from "styled-components";
 
-import MetabaseAnalytics, {
-  registerAnalyticsClickListener,
-} from "metabase/lib/analytics";
+import { trackPageView, createTracker } from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import api from "metabase/lib/api";
@@ -85,26 +83,16 @@ function _init(reducers, getRoutes, callback) {
     document.getElementById("root"),
   );
 
-  // listen for location changes and use that as a trigger for page view tracking
-  history.listen(location => {
-    MetabaseAnalytics.trackPageView(location.pathname);
-  });
+  createTracker(store);
+  trackPageView(location.pathname);
+  history.listen(location => trackPageView(location.pathname));
 
   registerVisualizations();
 
   initializeEmbedding(store);
 
-  registerAnalyticsClickListener();
-
   store.dispatch(refreshSiteSettings());
 
-  // enable / disable GA based on opt-out of anonymous tracking
-  MetabaseSettings.on("anon-tracking-enabled", () => {
-    window[
-      "ga-disable-" + MetabaseSettings.get("ga-code")
-    ] = MetabaseSettings.trackingEnabled() ? null : true;
-  });
-
   MetabaseSettings.on("user-locale", async locale => {
     // reload locale definition and site settings with the new locale
     await Promise.all([
diff --git a/frontend/src/metabase/auth/auth.js b/frontend/src/metabase/auth/auth.js
index 2a507cff9a3..2354852db5d 100644
--- a/frontend/src/metabase/auth/auth.js
+++ b/frontend/src/metabase/auth/auth.js
@@ -6,7 +6,7 @@ import {
 
 import { push } from "react-router-redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { clearGoogleAuthCredentials, deleteSession } from "metabase/lib/auth";
 
 import { refreshSiteSettings } from "metabase/redux/settings";
@@ -21,7 +21,7 @@ export const login = createThunkAction(
     // NOTE: this request will return a Set-Cookie header for the session
     await SessionApi.create(credentials);
 
-    MetabaseAnalytics.trackEvent("Auth", "Login");
+    MetabaseAnalytics.trackStructEvent("Auth", "Login");
 
     // unable to use a top-level `import` here because of a circular dependency
     const { refreshCurrentUser } = require("metabase/redux/user");
@@ -47,7 +47,7 @@ export const loginGoogle = createThunkAction(LOGIN_GOOGLE, function(
         token: googleUser.getAuthResponse().id_token,
       });
 
-      MetabaseAnalytics.trackEvent("Auth", "Google Auth Login");
+      MetabaseAnalytics.trackStructEvent("Auth", "Google Auth Login");
 
       // unable to use a top-level `import` here because of a circular dependency
       const { refreshCurrentUser } = require("metabase/redux/user");
@@ -74,7 +74,7 @@ export const logout = createThunkAction(LOGOUT, function() {
     // clear Google auth credentials if any are present
     await clearGoogleAuthCredentials();
 
-    MetabaseAnalytics.trackEvent("Auth", "Logout");
+    MetabaseAnalytics.trackStructEvent("Auth", "Logout");
 
     dispatch(push("/auth/login"));
 
diff --git a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
index 0feb1f0403a..36746418104 100644
--- a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
+++ b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
@@ -9,7 +9,7 @@ import Form from "metabase/containers/Form";
 import Icon from "metabase/components/Icon";
 
 import MetabaseSettings from "metabase/lib/settings";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { SessionApi } from "metabase/services";
 
@@ -51,7 +51,7 @@ export default class PasswordResetApp extends Component {
       password: password,
     });
 
-    MetabaseAnalytics.trackEvent("Auth", "Password Reset");
+    MetabaseAnalytics.trackStructEvent("Auth", "Password Reset");
     this.setState({ resetSuccess: true });
   };
 
diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
index c8956935d4d..3a890d811f0 100644
--- a/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddSeriesModal/AddSeriesModal.jsx
@@ -8,7 +8,7 @@ import { createSelector } from "reselect";
 
 import Visualization from "metabase/visualizations/components/Visualization";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { color } from "metabase/lib/colors";
 
 import Questions from "metabase/entities/questions";
@@ -90,7 +90,7 @@ export default class AddSeriesModal extends Component {
             series: this.state.series.concat(card),
           });
 
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "Dashboard",
             "Add Series",
             card.display + ", success",
@@ -102,7 +102,7 @@ export default class AddSeriesModal extends Component {
           });
           setTimeout(() => this.setState({ state: null }), 2000);
 
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "Dashboard",
             "Add Series",
             card.dataset_query.type + ", " + card.display + ", fail",
@@ -113,7 +113,7 @@ export default class AddSeriesModal extends Component {
           series: this.state.series.filter(c => c.id !== card.id),
         });
 
-        MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
+        MetabaseAnalytics.trackStructEvent("Dashboard", "Remove Series");
       }
     } catch (e) {
       console.error("AddSeriesModal handleQuestionChange", e);
@@ -127,7 +127,7 @@ export default class AddSeriesModal extends Component {
 
   handleRemoveSeries(card) {
     this.setState({ series: this.state.series.filter(c => c.id !== card.id) });
-    MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
+    MetabaseAnalytics.trackStructEvent("Dashboard", "Remove Series");
   }
 
   handleDone = () => {
@@ -136,7 +136,11 @@ export default class AddSeriesModal extends Component {
       attributes: { series: this.state.series },
     });
     this.props.onClose();
-    MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "done");
+    MetabaseAnalytics.trackStructEvent(
+      "Dashboard",
+      "Edit Series Modal",
+      "done",
+    );
   };
 
   handleLoadMetadata = async queries => {
diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal/QuestionList.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal/QuestionList.jsx
index 85da156dfe2..719a4cad89f 100644
--- a/frontend/src/metabase/dashboard/components/AddSeriesModal/QuestionList.jsx
+++ b/frontend/src/metabase/dashboard/components/AddSeriesModal/QuestionList.jsx
@@ -5,7 +5,7 @@ import { AutoSizer, List } from "react-virtualized";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import Icon from "metabase/components/Icon";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { useDebouncedValue } from "metabase/hooks/use-debounced-value";
 import { SEARCH_DEBOUNCE_DURATION } from "metabase/lib/constants";
 import EmptyState from "metabase/components/EmptyState";
@@ -55,7 +55,11 @@ export const QuestionList = React.memo(function QuestionList({
   );
 
   const handleSearchFocus = () => {
-    MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "search");
+    MetabaseAnalytics.trackStructEvent(
+      "Dashboard",
+      "Edit Series Modal",
+      "search",
+    );
   };
 
   const filteredQuestions = useMemo(() => {
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
index bb54ab3cbe0..9b949ae426e 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
@@ -9,7 +9,7 @@ import Modal from "metabase/components/Modal";
 import { PLUGIN_COLLECTIONS } from "metabase/plugins";
 
 import { getVisualizationRaw } from "metabase/visualizations";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { color } from "metabase/lib/colors";
 
 import {
@@ -112,7 +112,7 @@ export default class DashboardGrid extends Component {
 
     if (changes.length > 0) {
       setMultipleDashCardAttributes(changes);
-      MetabaseAnalytics.trackEvent("Dashboard", "Layout Changed");
+      MetabaseAnalytics.trackStructEvent("Dashboard", "Layout Changed");
     }
   };
 
diff --git a/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx b/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
index d0af4208822..63ebd3a9402 100644
--- a/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardSidebars.jsx
@@ -9,7 +9,7 @@ import ParameterSidebar from "metabase/parameters/components/ParameterSidebar";
 import SharingSidebar from "metabase/sharing/components/SharingSidebar";
 import { AddCardSidebar } from "./add-card-sidebar/AddCardSidebar";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 DashboardSidebars.propTypes = {
   dashboard: PropTypes.object,
@@ -75,7 +75,7 @@ export function DashboardSidebars({
         dashId: dashboard.id,
         cardId: cardId,
       });
-      MetabaseAnalytics.trackEvent("Dashboard", "Add Card");
+      MetabaseAnalytics.trackStructEvent("Dashboard", "Add Card");
     },
     [addCardToDashboard, dashboard.id],
   );
diff --git a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
index aeb6bd2a88c..8253d169d7e 100644
--- a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { t } from "ttag";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import Button from "metabase/components/Button";
 import ModalContent from "metabase/components/ModalContent";
@@ -21,7 +21,7 @@ export default class RemoveFromDashboardModal extends Component {
     });
     this.props.onClose();
 
-    MetabaseAnalytics.trackEvent("Dashboard", "Remove Card");
+    MetabaseAnalytics.trackStructEvent("Dashboard", "Remove Card");
   }
 
   render() {
diff --git a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx
index 86ac8bec089..1211f9cd695 100644
--- a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx
+++ b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx
@@ -25,7 +25,7 @@ import { getMetadata } from "metabase/selectors/metadata";
 
 import Dashboards from "metabase/entities/dashboards";
 import * as Urls from "metabase/lib/urls";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import * as Q from "metabase/lib/query/query";
 import Dimension from "metabase-lib/lib/Dimension";
 import { color } from "metabase/lib/colors";
@@ -80,7 +80,7 @@ class AutomaticDashboardApp extends React.Component {
     );
 
     this.setState({ savedDashboardId: newDashboard.id });
-    MetabaseAnalytics.trackEvent("AutoDashboard", "Save");
+    MetabaseAnalytics.trackStructEvent("AutoDashboard", "Save");
   };
 
   UNSAFE_componentWillReceiveProps(nextProps) {
@@ -164,7 +164,10 @@ class AutomaticDashboardApp extends React.Component {
                 to={more}
                 className="ml2"
                 onClick={() =>
-                  MetabaseAnalytics.trackEvent("AutoDashboard", "ClickMore")
+                  MetabaseAnalytics.trackStructEvent(
+                    "AutoDashboard",
+                    "ClickMore",
+                  )
                 }
               >
                 <Button iconRight="chevronright">{t`Show more about this`}</Button>
diff --git a/frontend/src/metabase/dashboard/containers/DashboardSharingEmbeddingModal.jsx b/frontend/src/metabase/dashboard/containers/DashboardSharingEmbeddingModal.jsx
index e859f5cd016..3612560901d 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardSharingEmbeddingModal.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardSharingEmbeddingModal.jsx
@@ -8,7 +8,7 @@ import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import EmbedModalContent from "metabase/public/components/widgets/EmbedModalContent";
 
 import * as Urls from "metabase/lib/urls";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import {
   createPublicLink,
@@ -60,7 +60,7 @@ class DashboardSharingEmbeddingModal extends Component {
             aria-disabled={!isLinkEnabled}
             onClick={() => {
               if (isLinkEnabled) {
-                MetabaseAnalytics.trackEvent(
+                MetabaseAnalytics.trackStructEvent(
                   "Sharing / Embedding",
                   "dashboard",
                   "Sharing Link Clicked",
diff --git a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
index 97d8a12b32d..886b70fed4e 100644
--- a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
+++ b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { replace } from "react-router-redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { parseHashOptions, stringifyHashOptions } from "metabase/lib/browser";
 
 import screenfull from "screenfull";
@@ -140,7 +140,7 @@ export default (ComposedComponent: React.Class) =>
           );
           this.setState({ refreshPeriod });
           this.setRefreshElapsed(0);
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "Dashboard",
             "Set Refresh",
             refreshPeriod,
diff --git a/frontend/src/metabase/entities/users.js b/frontend/src/metabase/entities/users.js
index c756146d89c..b0a00f3ea84 100644
--- a/frontend/src/metabase/entities/users.js
+++ b/frontend/src/metabase/entities/users.js
@@ -1,6 +1,6 @@
 import { assocIn } from "icepick";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import MetabaseUtils from "metabase/lib/utils";
 
@@ -71,12 +71,12 @@ const Users = createEntity({
 
   objectActions: {
     resentInvite: async ({ id }) => {
-      MetabaseAnalytics.trackEvent("People Admin", "Resent Invite");
+      MetabaseAnalytics.trackStructEvent("People Admin", "Resent Invite");
       await UserApi.send_invite({ id });
       return { type: RESEND_INVITE };
     },
     passwordResetEmail: async ({ email }) => {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "People Admin",
         "Trigger User Password Reset",
       );
@@ -87,18 +87,21 @@ const Users = createEntity({
       { id },
       password = MetabaseUtils.generatePassword(),
     ) => {
-      MetabaseAnalytics.trackEvent("People Admin", "Manual Password Reset");
+      MetabaseAnalytics.trackStructEvent(
+        "People Admin",
+        "Manual Password Reset",
+      );
       await UserApi.update_password({ id, password });
       return { type: PASSWORD_RESET_MANUAL, payload: { id, password } };
     },
     deactivate: async ({ id }) => {
-      MetabaseAnalytics.trackEvent("People Admin", "User Removed");
+      MetabaseAnalytics.trackStructEvent("People Admin", "User Removed");
       // TODO: move these APIs from services to this file
       await UserApi.delete({ userId: id });
       return { type: DEACTIVATE, payload: { id } };
     },
     reactivate: async ({ id }) => {
-      MetabaseAnalytics.trackEvent("People Admin", "User Reactivated");
+      MetabaseAnalytics.trackStructEvent("People Admin", "User Reactivated");
       // TODO: move these APIs from services to this file
       const user = await UserApi.reactivate({ userId: id });
       return { type: REACTIVATE, payload: user };
diff --git a/frontend/src/metabase/lib/CODENOTIFY b/frontend/src/metabase/lib/CODENOTIFY
new file mode 100644
index 00000000000..9e492b788ab
--- /dev/null
+++ b/frontend/src/metabase/lib/CODENOTIFY
@@ -0,0 +1,3 @@
+# See https://github.com/sourcegraph/codenotify for documentation.
+
+analytics.js @alxnddr @ranquild
diff --git a/frontend/src/metabase/lib/analytics.js b/frontend/src/metabase/lib/analytics.js
index c9eb1797456..1cdd60313e1 100644
--- a/frontend/src/metabase/lib/analytics.js
+++ b/frontend/src/metabase/lib/analytics.js
@@ -1,67 +1,107 @@
-/*global ga*/
+import * as Snowplow from "@snowplow/browser-tracker";
+import Settings from "metabase/lib/settings";
+import { isProduction } from "metabase/env";
+import { getUserId } from "metabase/selectors/user";
 
-import MetabaseSettings from "metabase/lib/settings";
+export const createTracker = store => {
+  if (isTrackingEnabled()) {
+    createGoogleAnalyticsTracker();
+    createSnowplowTracker(store);
+    document.body.addEventListener("click", handleStructEventClick, true);
+  }
+};
 
-import { DEBUG } from "metabase/lib/debug";
+export const trackPageView = url => {
+  if (isTrackingEnabled() && url) {
+    trackGoogleAnalyticsPageView(url);
+    trackSnowplowPageView(url);
+  }
+};
 
-// Simple module for in-app analytics.  Currently sends data to GA but could be extended to anything else.
-const MetabaseAnalytics = {
-  // track a pageview (a.k.a. route change)
-  trackPageView: function(url: string) {
-    if (url) {
-      // scrub query builder urls to remove serialized json queries from path
-      url = url.lastIndexOf("/q/", 0) === 0 ? "/q/" : url;
+export const trackStructEvent = (category, action, label, value) => {
+  if (isTrackingEnabled() && category && label) {
+    trackGoogleAnalyticsStructEvent(category, action, label, value);
+  }
+};
 
-      const { tag } = MetabaseSettings.get("version") || {};
+export const trackSchemaEvent = (schema, data) => {
+  if (isTrackingEnabled() && schema) {
+    trackSnowplowSchemaEvent(schema, data);
+  }
+};
 
-      if (typeof ga === "function") {
-        ga("set", "dimension1", tag);
-        ga("set", "page", url);
-        ga("send", "pageview", url);
-      }
-    }
-  },
+const isTrackingEnabled = () => {
+  return isProduction && Settings.trackingEnabled();
+};
 
-  // track an event
-  trackEvent: function(
-    category: string,
-    action?: ?string,
-    label?: ?(string | number | boolean),
-    value?: ?number,
-  ) {
-    const { tag } = MetabaseSettings.get("version") || {};
+const createGoogleAnalyticsTracker = () => {
+  const code = Settings.get("ga-code");
+  window.ga?.("create", code, "auto");
 
-    // category & action are required, rest are optional
-    if (typeof ga === "function" && category && action) {
-      ga("set", "dimension1", tag);
-      ga("send", "event", category, action, label, value);
-    }
-    if (DEBUG) {
-      console.log("trackEvent", { category, action, label, value });
-    }
-  },
+  Settings.on("anon-tracking-enabled", enabled => {
+    window[`ga-disable-${code}`] = enabled ? null : true;
+  });
 };
 
-export default MetabaseAnalytics;
+const trackGoogleAnalyticsPageView = url => {
+  const version = Settings.get("version");
+  window.ga?.("set", "dimension1", version?.tag);
+  window.ga?.("set", "page", url);
+  window.ga?.("send", "pageview", url);
+};
 
-export function registerAnalyticsClickListener() {
-  document.body.addEventListener(
-    "click",
-    function(e) {
-      let node = e.target;
+const trackGoogleAnalyticsStructEvent = (category, action, label, value) => {
+  const version = Settings.get("version");
+  window.ga?.("set", "dimension1", version?.tag);
+  window.ga?.("send", "event", category, action, label, value);
+};
 
-      // check the target and all parent elements
-      while (node) {
-        if (node.dataset && node.dataset.metabaseEvent) {
-          // we expect our event to be a semicolon delimited string
-          const parts = node.dataset.metabaseEvent
-            .split(";")
-            .map(p => p.trim());
-          MetabaseAnalytics.trackEvent(...parts);
-        }
-        node = node.parentNode;
-      }
+const createSnowplowTracker = store => {
+  Snowplow.newTracker("sp", "https://sp.metabase.com", {
+    appId: "metabase",
+    platform: "web",
+    cookieSameSite: "Lax",
+    discoverRootDomain: true,
+    contexts: {
+      webPage: true,
     },
-    true,
-  );
-}
+    plugins: [createSnowplowPlugin(store)],
+  });
+};
+
+const createSnowplowPlugin = store => {
+  return {
+    beforeTrack: () => {
+      const userId = getUserId(store.getState());
+      userId && Snowplow.setUserId(String(userId));
+    },
+  };
+};
+
+const trackSnowplowPageView = url => {
+  Snowplow.setReferrerUrl("#");
+  Snowplow.setCustomUrl(url);
+  Snowplow.trackPageView();
+};
+
+const trackSnowplowSchemaEvent = (schema, version, data) => {
+  Snowplow.trackSelfDescribingEvent({
+    event: {
+      schema: `iglu:com.metabase/${schema}/jsonschema/${version}`,
+      data,
+    },
+  });
+};
+
+const handleStructEventClick = event => {
+  if (!isTrackingEnabled()) {
+    return;
+  }
+
+  for (let node = event.target; node != null; node = node.parentNode) {
+    if (node.dataset && node.dataset.metabaseEvent) {
+      const parts = node.dataset.metabaseEvent.split(";").map(p => p.trim());
+      trackStructEvent(...parts);
+    }
+  }
+};
diff --git a/frontend/src/metabase/lib/redux.js b/frontend/src/metabase/lib/redux.js
index e8313b2e0dc..13284271e05 100644
--- a/frontend/src/metabase/lib/redux.js
+++ b/frontend/src/metabase/lib/redux.js
@@ -300,7 +300,7 @@ function withCachedData(getExistingStatePath, getRequestStatePath) {
       };
 }
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 export function withAnalytics(categoryOrFn, actionOrFn, labelOrFn, valueOrFn) {
   // thunk decorator:
@@ -319,7 +319,7 @@ export function withAnalytics(categoryOrFn, actionOrFn, labelOrFn, valueOrFn) {
           const action = get(actionOrFn, { category });
           const label = get(labelOrFn, { category, action });
           const value = get(valueOrFn, { category, action, label });
-          MetabaseAnalytics.trackEvent(category, action, label, value);
+          MetabaseAnalytics.trackStructEvent(category, action, label, value);
         } catch (error) {
           console.warn("withAnalytics threw an error:", error);
         }
diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
index 40bca0a0d4f..ca9a4351efc 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
@@ -23,7 +23,7 @@ import {
 } from "metabase/selectors/settings";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import type { Parameter, ParameterId } from "metabase-types/types/Parameter";
 import type {
@@ -202,7 +202,10 @@ export default class EmbedModalContent extends Component {
             name="close"
             size={24}
             onClick={() => {
-              MetabaseAnalytics.trackEvent("Sharing Modal", "Modal Closed");
+              MetabaseAnalytics.trackStructEvent(
+                "Sharing Modal",
+                "Modal Closed",
+              );
               onClose();
             }}
           />
diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
index 525a5bda74a..8117756ab0c 100644
--- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
@@ -12,7 +12,7 @@ import cx from "classnames";
 import type { EmbedType } from "./EmbedModalContent";
 import type { EmbeddableResource } from "metabase/public/lib/types";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 type Props = {
   resourceType: string,
@@ -72,7 +72,7 @@ export default class SharingPane extends Component {
                   title={t`Disable this public link?`}
                   content={t`This will cause the existing link to stop working. You can re-enable it, but when you do it will be a different link.`}
                   action={() => {
-                    MetabaseAnalytics.trackEvent(
+                    MetabaseAnalytics.trackStructEvent(
                       "Sharing Modal",
                       "Public Link Disabled",
                       resourceType,
@@ -86,7 +86,7 @@ export default class SharingPane extends Component {
                 <Toggle
                   value={false}
                   onChange={() => {
-                    MetabaseAnalytics.trackEvent(
+                    MetabaseAnalytics.trackStructEvent(
                       "Sharing Modal",
                       "Public Link Enabled",
                       resourceType,
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index 056994e1485..6b5bdcd4ebd 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -15,7 +15,7 @@ import ActionButton from "metabase/components/ActionButton";
 import Button from "metabase/components/Button";
 import DeleteModalWithConfirm from "metabase/components/DeleteModalWithConfirm";
 import Icon from "metabase/components/Icon";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import ModalContent from "metabase/components/ModalContent";
 import Subhead from "metabase/components/type/Subhead";
@@ -62,7 +62,7 @@ export default class PulseEdit extends Component {
     );
     this.props.fetchPulseFormInput();
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       this.props.pulseId ? "PulseEdit" : "PulseCreate",
       "Start",
     );
@@ -73,7 +73,7 @@ export default class PulseEdit extends Component {
     await this.props.updateEditingPulse(pulse);
     await this.props.saveEditingPulse();
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       this.props.pulseId ? "PulseEdit" : "PulseCreate",
       "Complete",
       this.props.pulse.cards.length,
@@ -88,7 +88,7 @@ export default class PulseEdit extends Component {
   handleArchive = async () => {
     await this.props.setPulseArchived(this.props.pulse, true);
 
-    MetabaseAnalytics.trackEvent("PulseArchive", "Complete");
+    MetabaseAnalytics.trackStructEvent("PulseArchive", "Complete");
 
     this.props.onChangeLocation(Urls.collection(this.props.collection));
   };
@@ -97,7 +97,7 @@ export default class PulseEdit extends Component {
     await this.props.setPulseArchived(this.props.pulse, false);
     this.setPulse({ ...this.props.pulse, archived: false });
 
-    MetabaseAnalytics.trackEvent("PulseUnarchive", "Complete");
+    MetabaseAnalytics.trackStructEvent("PulseUnarchive", "Complete");
   };
 
   setPulse = pulse => {
diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
index 253b16ea9fb..f4212ac3005 100644
--- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
@@ -9,7 +9,7 @@ import PulseCardPreview from "./PulseCardPreview";
 import QuestionSelect from "metabase/containers/QuestionSelect";
 
 // import Query from "metabase/lib/query";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { color } from "metabase/lib/colors";
 
@@ -56,7 +56,7 @@ export default class PulseEditCards extends Component {
   }
 
   trackPulseEvent = (eventName: string, eventValue: string) => {
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       this.props.pulseId ? "PulseEdit" : "PulseCreate",
       eventName,
       eventValue,
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index 2c5d4d1e4c4..41134c31da7 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -14,7 +14,7 @@ import Toggle from "metabase/components/Toggle";
 import Icon from "metabase/components/Icon";
 import ChannelSetupMessage from "metabase/components/ChannelSetupMessage";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { channelIsValid, createChannel } from "metabase/lib/pulse";
 
@@ -59,7 +59,7 @@ export default class PulseEditChannels extends Component {
 
     this.props.setPulse({ ...pulse, channels: pulse.channels.concat(channel) });
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       this.props.pulseId ? "PulseEdit" : "PulseCreate",
       "AddChannel",
       type,
@@ -86,7 +86,7 @@ export default class PulseEditChannels extends Component {
     const { pulse } = this.props;
     const channels = [...pulse.channels];
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       this.props.pulseId ? "PulseEdit" : "PulseCreate",
       channels[index].channel_type + ":" + changedProp.name,
       changedProp.value,
@@ -123,7 +123,7 @@ export default class PulseEditChannels extends Component {
         ),
       );
 
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         this.props.pulseId ? "PulseEdit" : "PulseCreate",
         "RemoveChannel",
         type,
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index f5f3751718f..98f6130bd32 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { t } from "ttag";
 import { recipientIsValid } from "metabase/lib/pulse";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import MetabaseUtils from "metabase/lib/utils";
 import TokenField from "metabase/components/TokenField";
@@ -41,7 +41,7 @@ export default class RecipientPicker extends Component {
       [...next].filter(r => !previous.has(r))[0] ||
       [...previous].filter(r => !next.has(r))[0];
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       isNewPulse ? "PulseCreate" : "PulseEdit",
       newRecipients.length > recipients.length
         ? "AddRecipient"
diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index c81b3bc3f16..8186c7f7eb2 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -13,7 +13,7 @@ import { push, replace } from "react-router-redux";
 import { setErrorPage } from "metabase/redux/app";
 import { loadMetadataForQuery } from "metabase/redux/metadata";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { startTimer } from "metabase/lib/performance";
 import {
   loadCard,
@@ -412,7 +412,7 @@ export const initializeQB = (location, params, queryParams) => {
           }
         }
 
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "QueryBuilder",
           "Query Loaded",
           card.dataset_query.type,
@@ -424,7 +424,7 @@ export const initializeQB = (location, params, queryParams) => {
         // if this is the users first time loading a saved card on the QB then show them the newb modal
         if (cardId && currentUser.is_qbnewb) {
           uiControls.isShowingNewbModal = true;
-          MetabaseAnalytics.trackEvent("QueryBuilder", "Show Newb Modal");
+          MetabaseAnalytics.trackStructEvent("QueryBuilder", "Show Newb Modal");
         }
 
         if (card.archived) {
@@ -483,7 +483,7 @@ export const initializeQB = (location, params, queryParams) => {
         }
       }
 
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "QueryBuilder",
         "Query Started",
         card.dataset_query.type,
@@ -567,7 +567,7 @@ export const initializeQB = (location, params, queryParams) => {
 
 export const TOGGLE_DATA_REFERENCE = "metabase/qb/TOGGLE_DATA_REFERENCE";
 export const toggleDataReference = createAction(TOGGLE_DATA_REFERENCE, () => {
-  MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Data Reference");
+  MetabaseAnalytics.trackStructEvent("QueryBuilder", "Toggle Data Reference");
 });
 
 export const TOGGLE_TEMPLATE_TAGS_EDITOR =
@@ -575,7 +575,10 @@ export const TOGGLE_TEMPLATE_TAGS_EDITOR =
 export const toggleTemplateTagsEditor = createAction(
   TOGGLE_TEMPLATE_TAGS_EDITOR,
   () => {
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Template Tags Editor");
+    MetabaseAnalytics.trackStructEvent(
+      "QueryBuilder",
+      "Toggle Template Tags Editor",
+    );
   },
 );
 
@@ -588,7 +591,7 @@ export const setIsShowingTemplateTagsEditor = isShowingTemplateTagsEditor => ({
 
 export const TOGGLE_SNIPPET_SIDEBAR = "metabase/qb/TOGGLE_SNIPPET_SIDEBAR";
 export const toggleSnippetSidebar = createAction(TOGGLE_SNIPPET_SIDEBAR, () => {
-  MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Snippet Sidebar");
+  MetabaseAnalytics.trackStructEvent("QueryBuilder", "Toggle Snippet Sidebar");
 });
 
 export const SET_IS_SHOWING_SNIPPET_SIDEBAR =
@@ -657,7 +660,7 @@ export const closeQbNewbModal = createThunkAction(CLOSE_QB_NEWB_MODAL, () => {
     // persist the fact that this user has seen the NewbModal
     const { currentUser } = getState();
     await UserApi.update_qbnewb({ id: currentUser.id });
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Close Newb Modal");
+    MetabaseAnalytics.trackStructEvent("QueryBuilder", "Close Newb Modal");
   };
 });
 
@@ -1021,7 +1024,7 @@ export const apiCreateQuestion = question => {
     dispatch(setRequestUnloaded(["entities", "databases"]));
 
     dispatch(updateUrl(createdQuestion.card(), { dirty: false }));
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Create Card",
       createdQuestion.query().datasetQuery().type,
@@ -1061,7 +1064,7 @@ export const apiUpdateQuestion = question => {
     // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
     dispatch(setRequestUnloaded(["entities", "databases"]));
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Update Card",
       updatedQuestion.query().datasetQuery().type,
@@ -1126,7 +1129,7 @@ export const runQuestionQuery = ({
       })
       .then(queryResults => {
         queryTimer(duration =>
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "QueryBuilder",
             "Run Query",
             question.query().datasetQuery().type,
diff --git a/frontend/src/metabase/query_builder/components/AlertModals.jsx b/frontend/src/metabase/query_builder/components/AlertModals.jsx
index 2b0a4df3fc2..a9f517bdfed 100644
--- a/frontend/src/metabase/query_builder/components/AlertModals.jsx
+++ b/frontend/src/metabase/query_builder/components/AlertModals.jsx
@@ -45,7 +45,7 @@ import {
   getDefaultAlert,
 } from "metabase-lib/lib/Alert";
 import MetabaseCookies from "metabase/lib/cookies";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 // types
 import type { AlertType } from "metabase-lib/lib/Alert";
@@ -129,7 +129,11 @@ export class CreateAlertModalContent extends Component {
     await updateUrl(question.card(), { dirty: false });
 
     onAlertCreated();
-    MetabaseAnalytics.trackEvent("Alert", "Create", alert.alert_condition);
+    MetabaseAnalytics.trackStructEvent(
+      "Alert",
+      "Create",
+      alert.alert_condition,
+    );
   };
 
   proceedFromEducationalScreen = () => {
@@ -329,7 +333,7 @@ export class UpdateAlertModalContent extends Component {
     await updateUrl(question.card(), { dirty: false });
     onAlertUpdated();
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "Alert",
       "Update",
       modifiedAlert.alert_condition,
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index e98de1d2557..7f476db23e8 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -11,7 +11,7 @@ import LimitWidget from "./LimitWidget";
 import SortWidget from "./SortWidget";
 import Popover from "metabase/components/Popover";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import type { DatasetQuery } from "metabase-types/types/Card";
@@ -58,7 +58,7 @@ export class ExtendedOptionsPopover extends Component {
       .updateExpression(name, expression, previousName)
       .update(setDatasetQuery);
     this.setState({ editExpression: null });
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Set Expression",
       !_.isEmpty(previousName),
@@ -70,13 +70,13 @@ export class ExtendedOptionsPopover extends Component {
     query.removeExpression(name).update(setDatasetQuery);
     this.setState({ editExpression: null });
 
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Remove Expression");
+    MetabaseAnalytics.trackStructEvent("QueryBuilder", "Remove Expression");
   }
 
   setLimit = limit => {
     const { query, setDatasetQuery } = this.props;
     query.updateLimit(limit).update(setDatasetQuery);
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Set Limit", limit);
+    MetabaseAnalytics.trackStructEvent("QueryBuilder", "Set Limit", limit);
     if (this.props.onClose) {
       this.props.onClose();
     }
@@ -146,7 +146,7 @@ export class ExtendedOptionsPopover extends Component {
         onAddExpression={() => this.setState({ editExpression: true })}
         onEditExpression={name => {
           this.setState({ editExpression: name });
-          MetabaseAnalytics.trackEvent(
+          MetabaseAnalytics.trackStructEvent(
             "QueryBuilder",
             "Show Edit Custom Field",
           );
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx b/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
index 180a59f53eb..802b84c10c9 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
@@ -5,7 +5,7 @@ import { t, jt } from "ttag";
 import { getFilterOptions, setFilterOptions } from "metabase/lib/query/filter";
 
 import CheckBox from "metabase/components/CheckBox";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import type { FieldFilter } from "metabase-types/types/Query";
 
@@ -79,7 +79,12 @@ export default class FilterOptions extends Component {
         [name]: !options[name],
       }),
     );
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Filter", "SetOption", name);
+    MetabaseAnalytics.trackStructEvent(
+      "QueryBuilder",
+      "Filter",
+      "SetOption",
+      name,
+    );
   }
 
   toggleOptionValue(name) {
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index 379d475efd1..640b9fe7c45 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -11,7 +11,7 @@ import CardTagEditor from "./CardTagEditor";
 import TagEditorHelp from "./TagEditorHelp";
 import SidebarContent from "metabase/query_builder/components/SidebarContent";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import type { DatasetQuery } from "metabase-types/types/Card";
@@ -54,7 +54,7 @@ export default class TagEditorSidebar extends React.Component {
 
   setSection(section) {
     this.setState({ section: section });
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Template Tag Editor Section Change",
       section,
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index 8b5683b55b3..15f4b3bd381 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -10,7 +10,7 @@ import EmbedModalContent from "metabase/public/components/widgets/EmbedModalCont
 
 import * as Urls from "metabase/lib/urls";
 import MetabaseSettings from "metabase/lib/settings";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { getParametersFromCard } from "metabase/meta/Card";
 import {
@@ -99,7 +99,7 @@ export function QuestionEmbedWidgetTrigger({ onClick }) {
       tooltip={t`Sharing`}
       className="mx1 hide sm-show text-brand-hover cursor-pointer"
       onClick={() => {
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Sharing / Embedding",
           "question",
           "Sharing Link Clicked",
diff --git a/frontend/src/metabase/redux/undo.js b/frontend/src/metabase/redux/undo.js
index a889791c2d9..0276f58a639 100644
--- a/frontend/src/metabase/redux/undo.js
+++ b/frontend/src/metabase/redux/undo.js
@@ -1,7 +1,7 @@
 import _ from "underscore";
 
 import { createAction, createThunkAction } from "metabase/lib/redux";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 const ADD_UNDO = "metabase/questions/ADD_UNDO";
 const DISMISS_UNDO = "metabase/questions/DISMISS_UNDO";
@@ -21,7 +21,7 @@ export const dismissUndo = createAction(
   DISMISS_UNDO,
   (undoId, track = true) => {
     if (track) {
-      MetabaseAnalytics.trackEvent("Undo", "Dismiss Undo");
+      MetabaseAnalytics.trackStructEvent("Undo", "Dismiss Undo");
     }
     return undoId;
   },
@@ -29,7 +29,7 @@ export const dismissUndo = createAction(
 
 export const performUndo = createThunkAction(PERFORM_UNDO, undoId => {
   return (dispatch, getState) => {
-    MetabaseAnalytics.trackEvent("Undo", "Perform Undo");
+    MetabaseAnalytics.trackStructEvent("Undo", "Perform Undo");
     const undo = _.findWhere(getState().undo, { id: undoId });
     if (undo) {
       undo.actions.map(action => dispatch(action));
diff --git a/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx b/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
index 757d7420442..8e184af2f8e 100644
--- a/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import MetricSidebar from "./MetricSidebar";
 import SidebarLayout from "metabase/components/SidebarLayout";
@@ -56,7 +56,7 @@ export default class MetricDetailContainer extends Component {
   startEditing() {
     const { metric, router } = this.props;
     router.replace(`/reference/metrics/${metric.id}/edit`);
-    MetabaseAnalytics.trackEvent("Data Reference", "Started Editing");
+    MetabaseAnalytics.trackStructEvent("Data Reference", "Started Editing");
   }
 
   endEditing() {
diff --git a/frontend/src/metabase/reference/reference.js b/frontend/src/metabase/reference/reference.js
index 38062f744a2..c6569684edc 100644
--- a/frontend/src/metabase/reference/reference.js
+++ b/frontend/src/metabase/reference/reference.js
@@ -2,7 +2,7 @@ import { assoc } from "icepick";
 
 import { handleActions, createAction } from "metabase/lib/redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { filterUntouchedFields, isEmptyObject } from "./utils.js";
 
@@ -26,11 +26,11 @@ export const startLoading = createAction(START_LOADING);
 export const endLoading = createAction(END_LOADING);
 
 export const startEditing = createAction(START_EDITING, () => {
-  MetabaseAnalytics.trackEvent("Data Reference", "Started Editing");
+  MetabaseAnalytics.trackStructEvent("Data Reference", "Started Editing");
 });
 
 export const endEditing = createAction(END_EDITING, () => {
-  MetabaseAnalytics.trackEvent("Data Reference", "Ended Editing");
+  MetabaseAnalytics.trackStructEvent("Data Reference", "Ended Editing");
 });
 
 export const expandFormula = createAction(EXPAND_FORMULA);
diff --git a/frontend/src/metabase/setup/actions.js b/frontend/src/metabase/setup/actions.js
index 422f172840b..38cd0aaea1a 100644
--- a/frontend/src/metabase/setup/actions.js
+++ b/frontend/src/metabase/setup/actions.js
@@ -1,7 +1,7 @@
 import { createAction } from "redux-actions";
 import { createThunkAction } from "metabase/lib/redux";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import { SetupApi, UtilApi } from "metabase/services";
@@ -70,7 +70,7 @@ export const submitSetup = createThunkAction(SUBMIT_SETUP, function() {
 
       return null;
     } catch (error) {
-      MetabaseAnalytics.trackEvent("Setup", "Error", "save");
+      MetabaseAnalytics.trackStructEvent("Setup", "Error", "save");
 
       return error;
     }
diff --git a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
index 93641dc7c23..a7d0771e533 100644
--- a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
@@ -8,7 +8,7 @@ import { Box } from "grid-styled";
 import StepTitle from "./StepTitle";
 import CollapsedStep from "./CollapsedStep";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { DEFAULT_SCHEDULES } from "metabase/admin/databases/database";
 import Databases from "metabase/entities/databases";
@@ -27,7 +27,7 @@ export default class DatabaseConnectionStep extends Component {
 
   chooseDatabaseEngine = e => {
     // FIXME:
-    // MetabaseAnalytics.trackEvent("Setup", "Choose Database", engine);
+    // MetabaseAnalytics.trackStructEvent("Setup", "Choose Database", engine);
   };
 
   handleSubmit = async database => {
@@ -49,7 +49,7 @@ export default class DatabaseConnectionStep extends Component {
       }
 
       if (formError) {
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Setup",
           "Error",
           "database validation: " + database.engine,
@@ -81,7 +81,11 @@ export default class DatabaseConnectionStep extends Component {
         details: database,
       });
 
-      MetabaseAnalytics.trackEvent("Setup", "Database Step", database.engine);
+      MetabaseAnalytics.trackStructEvent(
+        "Setup",
+        "Database Step",
+        database.engine,
+      );
     }
   };
 
@@ -91,7 +95,7 @@ export default class DatabaseConnectionStep extends Component {
       details: null,
     });
 
-    MetabaseAnalytics.trackEvent("Setup", "Database Step");
+    MetabaseAnalytics.trackStructEvent("Setup", "Database Step");
   };
 
   render() {
diff --git a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
index 7a12febc115..6f3ff24825b 100644
--- a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
@@ -10,7 +10,7 @@ import Icon from "metabase/components/Icon";
 
 import Databases from "metabase/entities/databases";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 export default class DatabaseSchedulingStep extends Component {
   static propTypes = {
@@ -28,7 +28,11 @@ export default class DatabaseSchedulingStep extends Component {
       details: database,
     });
 
-    MetabaseAnalytics.trackEvent("Setup", "Database Step", this.state.engine);
+    MetabaseAnalytics.trackStructEvent(
+      "Setup",
+      "Database Step",
+      this.state.engine,
+    );
   };
 
   render() {
diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx
index 67e764c5c96..37904d8c402 100644
--- a/frontend/src/metabase/setup/components/PreferencesStep.jsx
+++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { t, jt } from "ttag";
 import { Box } from "grid-styled";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import Toggle from "metabase/components/Toggle";
 
@@ -42,7 +42,7 @@ export default class PreferencesStep extends Component {
       payload && payload.data ? getErrorMessage(payload.data) : null;
     this.setState({ errorMessage });
 
-    MetabaseAnalytics.trackEvent(
+    MetabaseAnalytics.trackStructEvent(
       "Setup",
       "Preferences Step",
       this.props.allowTracking,
diff --git a/frontend/src/metabase/setup/components/Setup.jsx b/frontend/src/metabase/setup/components/Setup.jsx
index 86255c7ef9d..39323b8c13c 100644
--- a/frontend/src/metabase/setup/components/Setup.jsx
+++ b/frontend/src/metabase/setup/components/Setup.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { t } from "ttag";
 
 import { color } from "metabase/lib/colors";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import { b64hash_to_utf8 } from "metabase/lib/encoding";
 
@@ -50,7 +50,7 @@ export default class Setup extends Component {
 
   completeWelcome() {
     this.props.setActiveStep(LANGUAGE_STEP_NUMBER);
-    MetabaseAnalytics.trackEvent("Setup", "Welcome");
+    MetabaseAnalytics.trackStructEvent("Setup", "Welcome");
   }
 
   componentDidMount() {
@@ -117,7 +117,7 @@ export default class Setup extends Component {
     }
 
     if (!prevProps.setupComplete && this.props.setupComplete) {
-      MetabaseAnalytics.trackEvent("Setup", "Complete");
+      MetabaseAnalytics.trackStructEvent("Setup", "Complete");
     }
   }
 
diff --git a/frontend/src/metabase/setup/components/UserStep.jsx b/frontend/src/metabase/setup/components/UserStep.jsx
index 8e3ea0e9e64..1f2eccc7797 100644
--- a/frontend/src/metabase/setup/components/UserStep.jsx
+++ b/frontend/src/metabase/setup/components/UserStep.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Flex, Box } from "grid-styled";
 import { t } from "ttag";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import User from "metabase/entities/users";
 
@@ -29,7 +29,11 @@ export default class UserStep extends Component {
       await this.props.validatePassword(values.password);
       return {};
     } catch (error) {
-      MetabaseAnalytics.trackEvent("Setup", "Error", "password validation");
+      MetabaseAnalytics.trackStructEvent(
+        "Setup",
+        "Error",
+        "password validation",
+      );
       return error.data.errors;
     }
   };
@@ -40,7 +44,7 @@ export default class UserStep extends Component {
       details: _.omit(values, "password_confirm"),
     });
 
-    MetabaseAnalytics.trackEvent("Setup", "User Details Step");
+    MetabaseAnalytics.trackStructEvent("Setup", "User Details Step");
   };
 
   render() {
diff --git a/frontend/src/metabase/visualizations/components/CardRenderer.jsx b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
index 201b5d244a6..b996c9997d5 100644
--- a/frontend/src/metabase/visualizations/components/CardRenderer.jsx
+++ b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
@@ -5,7 +5,7 @@ import ReactDOM from "react-dom";
 import _ from "underscore";
 
 import ExplicitSize from "metabase/components/ExplicitSize";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { startTimer } from "metabase/lib/performance";
 
 import { isSameSeries } from "metabase/visualizations/lib/utils";
@@ -21,7 +21,10 @@ type Props = VisualizationProps & {
 
 // We track this as part of the render loop.
 // It's throttled to prevent pounding GA on every prop update.
-const trackEventThrottled = _.throttle(MetabaseAnalytics.trackEvent, 10000);
+const trackEventThrottled = _.throttle(
+  MetabaseAnalytics.trackStructEvent,
+  10000,
+);
 
 @ExplicitSize({ wrapped: true })
 export default class CardRenderer extends Component {
diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
index 779eb324c1c..b1cfacf96d5 100644
--- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
@@ -10,7 +10,7 @@ import Tooltip from "metabase/components/Tooltip";
 
 import "./ChartClickActions.css";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import { performAction } from "metabase/visualizations/lib/action";
 
@@ -104,7 +104,7 @@ export default class ChartClickActions extends Component {
   handleClickAction = (action: ClickAction) => {
     const { dispatch, onChangeCardAndRun } = this.props;
     if (action.popover) {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Actions",
         "Open Click Action Popover",
         getGALabelForAction(action),
@@ -116,7 +116,7 @@ export default class ChartClickActions extends Component {
         onChangeCardAndRun,
       });
       if (didPerform) {
-        MetabaseAnalytics.trackEvent(
+        MetabaseAnalytics.trackStructEvent(
           "Actions",
           "Executed Click Action",
           getGALabelForAction(action),
@@ -143,7 +143,7 @@ export default class ChartClickActions extends Component {
         <PopoverContent
           onChangeCardAndRun={({ nextCard }) => {
             if (popoverAction) {
-              MetabaseAnalytics.trackEvent(
+              MetabaseAnalytics.trackStructEvent(
                 "Action",
                 "Executed Click Action",
                 getGALabelForAction(popoverAction),
@@ -152,7 +152,7 @@ export default class ChartClickActions extends Component {
             onChangeCardAndRun({ nextCard });
           }}
           onClose={() => {
-            MetabaseAnalytics.trackEvent(
+            MetabaseAnalytics.trackStructEvent(
               "Action",
               "Dismissed Click Action Menu",
               getGALabelForAction(popoverAction),
@@ -194,7 +194,10 @@ export default class ChartClickActions extends Component {
         target={clicked.element}
         targetEvent={clicked.event}
         onClose={() => {
-          MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu");
+          MetabaseAnalytics.trackStructEvent(
+            "Action",
+            "Dismissed Click Action Menu",
+          );
           this.close();
         }}
         verticalAttachments={["top", "bottom"]}
@@ -310,7 +313,7 @@ export const ChartClickAction = ({
           to={action.url()}
           className={className}
           onClick={() =>
-            MetabaseAnalytics.trackEvent(
+            MetabaseAnalytics.trackStructEvent(
               "Actions",
               "Executed Click Action",
               getGALabelForAction(action),
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 0a193f84bf1..7c480323825 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -13,7 +13,7 @@ import Visualization from "metabase/visualizations/components/Visualization";
 import ChartSettingsWidget from "./ChartSettingsWidget";
 
 import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import {
   getVisualizationTransformed,
   extractRemappings,
@@ -92,7 +92,7 @@ class ChartSettings extends Component {
   };
 
   handleResetSettings = () => {
-    MetabaseAnalytics.trackEvent("Chart Settings", "Reset Settings");
+    MetabaseAnalytics.trackStructEvent("Chart Settings", "Reset Settings");
 
     const settings = getClickBehaviorSettings(this._getSettings());
     this.props.onChange(settings);
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index ef75b79850d..3633e3d7afd 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -9,7 +9,7 @@ import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
 import { t, jt } from "ttag";
 import { duration, formatNumber } from "metabase/lib/formatting";
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 import {
   getVisualizationTransformed,
@@ -317,7 +317,7 @@ export default class Visualization extends React.PureComponent {
 
   handleVisualizationClick = (clicked: ClickObject) => {
     if (clicked) {
-      MetabaseAnalytics.trackEvent(
+      MetabaseAnalytics.trackStructEvent(
         "Actions",
         "Clicked",
         `${clicked.column ? "column" : ""} ${clicked.value ? "value" : ""} ${
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx
index fead4d55d43..6b765b24191 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx
@@ -19,7 +19,7 @@ import {
   SortableElement,
 } from "metabase/components/sortable";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 import { isNumeric, isString } from "metabase/lib/schema_metadata";
 
 import _ from "underscore";
@@ -145,7 +145,7 @@ export default class ChartSettingsTableFormatting extends React.Component {
           }}
           onRemove={index => {
             onChange([...value.slice(0, index), ...value.slice(index + 1)]);
-            MetabaseAnalytics.trackEvent(
+            MetabaseAnalytics.trackStructEvent(
               "Chart Settings",
               "Table Formatting",
               "Remove Rule",
@@ -155,7 +155,7 @@ export default class ChartSettingsTableFormatting extends React.Component {
             const newValue = [...value];
             newValue.splice(to, 0, newValue.splice(from, 1)[0]);
             onChange(newValue);
-            MetabaseAnalytics.trackEvent(
+            MetabaseAnalytics.trackStructEvent(
               "Chart Settings",
               "Table Formatting",
               "Move Rule",
@@ -397,7 +397,7 @@ const RuleEditor = ({ rule, cols, isNew, onChange, onDone, onRemove }) => {
           <ColorRangePicker
             value={rule.colors}
             onChange={colors => {
-              MetabaseAnalytics.trackEvent(
+              MetabaseAnalytics.trackStructEvent(
                 "Chart Settings",
                 "Table Formatting",
                 "Select Range  Colors",
diff --git a/frontend/src/metabase/visualizations/lib/settings.js b/frontend/src/metabase/visualizations/lib/settings.js
index d2217084fd9..ab684079e56 100644
--- a/frontend/src/metabase/visualizations/lib/settings.js
+++ b/frontend/src/metabase/visualizations/lib/settings.js
@@ -15,7 +15,7 @@ import ChartSettingFieldsPartition from "metabase/visualizations/components/sett
 import ChartSettingColorPicker from "metabase/visualizations/components/settings/ChartSettingColorPicker";
 import ChartSettingColorsPicker from "metabase/visualizations/components/settings/ChartSettingColorsPicker";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
+import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 export type SettingId = string;
 
@@ -251,7 +251,7 @@ export function updateSettings(
   changedSettings: Settings,
 ): Settings {
   for (const key of Object.keys(changedSettings)) {
-    MetabaseAnalytics.trackEvent("Chart Settings", "Change Setting", key);
+    MetabaseAnalytics.trackStructEvent("Chart Settings", "Change Setting", key);
   }
   const newSettings = {
     ...storedSettings,
diff --git a/frontend/test/__support__/mocks.js b/frontend/test/__support__/mocks.js
index 0ab0ba88e41..f85aa87319a 100644
--- a/frontend/test/__support__/mocks.js
+++ b/frontend/test/__support__/mocks.js
@@ -1,6 +1,7 @@
 /* eslint-disable no-import-assign*/
 
 global.ga = () => {};
+global.snowplow = () => {};
 global.ace.define = () => {};
 global.ace.require = () => {};
 
diff --git a/package.json b/package.json
index 28a9592466a..080c36e9e6e 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
     "yarn": ">=1.12.3"
   },
   "dependencies": {
+    "@snowplow/browser-tracker": "^3.1.6",
     "@visx/axis": "1.8.0",
     "@visx/grid": "1.16.0",
     "@visx/group": "1.7.0",
diff --git a/resources/frontend_client/inline_js/index_ganalytics.js b/resources/frontend_client/inline_js/index_ganalytics.js
index 7923de834ff..f825107dbd6 100644
--- a/resources/frontend_client/inline_js/index_ganalytics.js
+++ b/resources/frontend_client/inline_js/index_ganalytics.js
@@ -1,12 +1,3 @@
 (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','https://www.google-analytics.com/analytics.js','ga');
-
-// if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview
-const tracking = window.MetabaseBootstrap["anon-tracking-enabled"];
-const ga_code = window.MetabaseBootstrap["ga-code"];
-if (!tracking) {
-  window['ga-disable-'+ga_code] = true;
-}
-
-ga('create', ga_code, 'auto');
diff --git a/src/metabase/server/middleware/security.clj b/src/metabase/server/middleware/security.clj
index e10e2b31ce1..4c0e9ac1202 100644
--- a/src/metabase/server/middleware/security.clj
+++ b/src/metabase/server/middleware/security.clj
@@ -80,6 +80,9 @@
                                  ;; Google analytics
                                  (when (public-settings/anon-tracking-enabled)
                                    "www.google-analytics.com")
+                                 ;; Snowplow analytics
+                                 (when (public-settings/anon-tracking-enabled)
+                                   "sp.metabase.com")
                                  ;; Webpack dev server
                                  (when config/is-dev?
                                    "localhost:8080 ws://localhost:8080")]
diff --git a/yarn.lock b/yarn.lock
index 5477d5bee17..a022133e538 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2147,6 +2147,33 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
+"@snowplow/browser-tracker-core@3.1.6":
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/@snowplow/browser-tracker-core/-/browser-tracker-core-3.1.6.tgz#660e62311d5aafa9769d3e8ac315bba1939ff0ca"
+  integrity sha512-IOsQaI5EOaaOHxxgIGAeSZIR7oM8sre0dDjgBwqsUFpQgpha2/c5mP0ui59hXhBzGFlXA+10wml50OXx/YwMCw==
+  dependencies:
+    "@snowplow/tracker-core" "3.1.6"
+    sha1 "^1.1.1"
+    tslib "^2.3.0"
+    uuid "^3.4.0"
+
+"@snowplow/browser-tracker@^3.1.6":
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/@snowplow/browser-tracker/-/browser-tracker-3.1.6.tgz#f8f41b12c79e9886fda7d10924a6a487112d5cf6"
+  integrity sha512-6VY/3cgEvRzE6u52n0OSZR9pSjFzud4cICNIZrH2YVcF8/lCrHKscatBVffALrLShSglSB4FZPG9CtfPCAivow==
+  dependencies:
+    "@snowplow/browser-tracker-core" "3.1.6"
+    "@snowplow/tracker-core" "3.1.6"
+    tslib "^2.3.0"
+
+"@snowplow/tracker-core@3.1.6":
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/@snowplow/tracker-core/-/tracker-core-3.1.6.tgz#f1f87782e64a2d59ab881cbb7e6e1a1e43972283"
+  integrity sha512-b2O1xXEqQRzuhIol+3mhk9Xvz/smiW+6pQ3W1fVWonXrYoKtTyak2K+98phj2e0V+H/jUXeviH8adLEdbRkCYA==
+  dependencies:
+    tslib "^2.3.0"
+    uuid "^3.4.0"
+
 "@testing-library/cypress@^5.0.2":
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-5.3.1.tgz#96c9bd0f72eb2330b4b5154dd71e81d2c644ce62"
@@ -4259,6 +4286,11 @@ character-reference-invalid@^1.0.0:
   resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
   integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
 
+"charenc@>= 0.0.1":
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+  integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
+
 check-more-types@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
@@ -5219,6 +5251,11 @@ crossfilter@^1.3.12:
   resolved "https://registry.yarnpkg.com/crossfilter/-/crossfilter-1.3.12.tgz#147d7236a98c45c69f78bdc3a99d6fb00f70930c"
   integrity sha1-FH1yNqmMRcafeL3DqZ1vsA9wkww=
 
+"crypt@>= 0.0.1":
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+  integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
+
 crypto-browserify@^3.11.0, crypto-browserify@^3.12.0:
   version "3.12.0"
   resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -13997,6 +14034,14 @@ sha.js@^2.4.0, sha.js@^2.4.8:
     inherits "^2.0.1"
     safe-buffer "^5.0.1"
 
+sha1@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848"
+  integrity sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=
+  dependencies:
+    charenc ">= 0.0.1"
+    crypt ">= 0.0.1"
+
 shadow-cljs-jar@1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
@@ -15160,6 +15205,11 @@ tslib@^2.0.3:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
   integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
 
+tslib@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+  integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+
 tsutils@^3.21.0:
   version "3.21.0"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
-- 
GitLab