Skip to content

Product Analytics dashboard widgets structure

What does this MR do and why?

This MR adds the ability to define customizable dashboards with individual widgets. To build the dashboards, it uses a new NPM package called Gridstack.

This MR adds a placeholder widget to make each part testable. The placeholder widget will be removed with the next iteration where we create the real widgets we wish to use.

The widget configuration is intentionally a bit vague for two reasons:

  1. We don't want to limit what a widget can do.
  2. As we build widgets we'll learn what we need and can narrow the focus further.

A detailed discussion on the widget config is already underway: Cube Query Rendering Widget - Line Chart (#377708 - closed)

An example configuration that works for use with the Cube JS library
widgets: [
  {
    component: 'ProductAnalyticsAudience',
    title: s__('ProductAnalytics|Users / Time'),
    gridAttributes: {
      size: {
        height: 4,
        width: 6,
        minHeight: 4,
        minWidth: 6,
      },
      position: {
        xPos: 0,
        yPos: 0,
      },
    },
    chartOptions: {
      xAxis: { name: s__('ProductAnalytics|Time'), type: 'time' },
      yAxis: { name: s__('ProductAnalytics|Counts') },
    },
    data: {
      query: {
        users: {
          measures: ['Jitsu.count'],
          dimensions: ['Jitsu.eventType'],
        },
      },
    },
  },
]

Screenshots or screen recordings

Screenshot of mock audience widget

image

Recording with a test graph

Screen_Recording_2022-10-14_at_12.40.12

How to set up and validate locally

Make sure you are on GitLab Ultimate.

Testing this MR specifically
  1. Enable the feature flag: echo "Feature.enable(:product_analytics_internal_preview)" | rails c.
  2. Visit any project pages such as http://127.0.0.1:3000/groups/flightjs/Flight/-/product_analytics/dashboards and validate that the widget appears.
Testing with a Cube JS graph (for how it will be used in future, only test if you **really** want to)
  1. Set up the devkit and start it running using docker-compose up.
  2. Open the rails console: rails c
  3. Enable the feature flag: Feature.enable(:cube_api_proxy).
  4. Update the application settings (details obtained through devkit setup):
ApplicationSetting.current.update(jitsu_project_xid: 'XXXXXXXXXX', jitsu_host: 'http://localhost:8000', clickhouse_connection_string: 'http://test:test@clickhouse:8123', jitsu_administrator_email: 'XXXXXXX', jitsu_administrator_password: 'XXXXXXX', cube_api_base_url: 'http://localhost:4000', cube_api_key: API KEY FROM CUBE .env)
reload!
  1. Find a project on the console: project = Project.find(PROJECT_ID)
  2. Run ProductAnalytics::InitializeStackService.new(container: project).execute
  3. Ensure that a sidekiq job was enqueued and in the Jitsu configurator the new API key is created.
  4. Copy the JS API key and add the JS SDK to a local page to start generating data.
  5. Run yarn install on the root of your GitLab instance.
  6. Apply the following patch, and you should then see a graph of some data:
Index: config/webpack.config.js
<+>UTF-8
===================================================================
diff --git a/config/webpack.config.js b/config/webpack.config.js
--- a/config/webpack.config.js  (revision Staged)
+++ b/config/webpack.config.js  (date 1665745865927)
@@ -305,6 +305,11 @@
         test: /\.mjs$/,
         use: [],
       },
+      {
+        test: /(@cubejs-client\/vue).*\.(js)?$/,
+        include: /node_modules/,
+        loader: 'babel-loader',
+      },
       WEBPACK_USE_ESBUILD_LOADER && {
         test: /\.js$/,
         exclude: (modulePath) =>
Index: ee/app/assets/javascripts/product_analytics/dashboards/components/widgets/audience.vue
<+>UTF-8
===================================================================
diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/components/widgets/audience.vue b/ee/app/assets/javascripts/product_analytics/dashboards/components/widgets/audience.vue
--- a/ee/app/assets/javascripts/product_analytics/dashboards/components/widgets/audience.vue    (revision Staged)
+++ b/ee/app/assets/javascripts/product_analytics/dashboards/components/widgets/audience.vue    (date 1665747023884)
@@ -1,8 +1,16 @@
 <script>
-import { s__ } from '~/locale';
+import { QueryRenderer } from '@cubejs-client/vue';
+import { GlLineChart } from '@gitlab/ui/dist/charts';
+import { CubejsApi } from '@cubejs-client/core';
+
+const cubejsApi = new CubejsApi('1', { apiUrl: '/api/v4/projects/1/product_analytics/request' });
 
 export default {
   name: 'AudienceWidget',
+  components: {
+    GlLineChart,
+    QueryRenderer,
+  },
   props: {
     data: {
       type: Object,
@@ -20,12 +28,42 @@
       default: () => ({}),
     },
   },
-  i18n: {
-    content: s__('ProductAnalytics|Widgets content'),
+  data() {
+    return { cubejsApi };
+  },
+  methods: {
+    series(resultSet) {
+      if (!resultSet) {
+        return [];
+      }
+
+      const seriesNames = resultSet?.seriesNames();
+      const pivot = resultSet?.chartPivot();
+      const series = [];
+
+      seriesNames.forEach((e) => {
+        const data = pivot.map((p) => [p.x, p[e.key]]);
+        series.push({
+          name: e.title,
+          data,
+        });
+      });
+
+      return series;
+    },
   },
 };
 </script>
 
 <template>
-  <p>{{ $options.i18n.content }}</p>
+  <query-renderer :cubejs-api="cubejsApi" :query="data.query">
+    <template #default="{ resultSet }">
+      <gl-line-chart
+        :data="series(resultSet)"
+        :option="chartOptions"
+        :show-legend="!customizations.hideLegend"
+        responsive
+      />
+    </template>
+  </query-renderer>
 </template>
Index: ee/app/assets/javascripts/product_analytics/dashboards/components/analytics_dashboard.vue
<+>UTF-8
===================================================================
diff --git a/ee/app/assets/javascripts/product_analytics/dashboards/components/analytics_dashboard.vue b/ee/app/assets/javascripts/product_analytics/dashboards/components/analytics_dashboard.vue
--- a/ee/app/assets/javascripts/product_analytics/dashboards/components/analytics_dashboard.vue (revision Staged)
+++ b/ee/app/assets/javascripts/product_analytics/dashboards/components/analytics_dashboard.vue (date 1665747551823)
@@ -2,6 +2,19 @@
 import CustomizableDashboard from 'ee/vue_shared/components/customizable_dashboard/customizable_dashboard.vue';
 import { s__ } from '~/locale';
 
+const overTimeQueries = {
+  users: {
+    measures: ['Jitsu.count'],
+    timeDimensions: [
+      {
+        granularity: 'day',
+        dimension: 'Jitsu.utcTime',
+      },
+    ],
+    dimensions: ['Jitsu.eventType'],
+  },
+};
+
 export default {
   name: 'AnalyticsDashboard',
   components: {
@@ -15,13 +28,55 @@
           title: s__('ProductAnalytics|Audience'),
           gridAttributes: {
             size: {
-              width: 3,
-              height: 3,
+              minHeight: 6,
+              minWidth: 5,
             },
+            position: {
+              xPos: 0,
+              yPos: 0,
+            },
           },
+          data: {
+            query: this.withFilters(overTimeQueries.users, 'day'),
+          },
+          chartOptions: {
+            xAxis: {
+              name: s__('ProductAnalytics|Time'),
+              type: 'time',
+              axisLabel: {
+                interval: 0,
+                showMinLabel: false,
+                showMaxLabel: false,
+                align: 'right',
+              },
+            },
+            yAxis: {
+              name: s__('ProductAnalytics|Counts'),
+            },
+          },
+          customizations: {
+            hideLegend: true,
+          },
         },
       ],
     };
+  },
+  methods: {
+    withFilters(query, granularity) {
+      const newQuery = {
+        ...query,
+      };
+
+      newQuery.timeDimensions = [
+        {
+          dimension: `Jitsu.utcTime`,
+          dateRange: [new Date(2022, 8), new Date(2022, 10)],
+          granularity: granularity || null,
+        },
+      ];
+
+      return newQuery;
+    },
   },
 };
 </script>
Index: jest.config.base.js
<+>UTF-8
===================================================================
diff --git a/jest.config.base.js b/jest.config.base.js
--- a/jest.config.base.js   (revision Staged)
+++ b/jest.config.base.js   (date 1665746037488)
@@ -153,6 +153,7 @@
     'dateformat',
     'lowlight',
     'vscode-languageserver-types',
+    '@cubejs-client/vue',
     ...gfmParserDependencies,
   ];
 
Index: package.json
<+>UTF-8
===================================================================
diff --git a/package.json b/package.json
--- a/package.json  (revision Staged)
+++ b/package.json  (date 1665746037500)
@@ -50,6 +50,8 @@
     "@babel/core": "^7.18.5",
     "@babel/preset-env": "^7.18.2",
     "@codesandbox/sandpack-client": "^1.2.2",
+    "@cubejs-client/core": "^0.31.0",
+    "@cubejs-client/vue": "^0.31.0",
     "@gitlab/at.js": "1.5.7",
     "@gitlab/favicon-overlay": "2.0.0",
     "@gitlab/svgs": "3.4.0",
@@ -268,4 +270,4 @@
     "node": ">=12.22.1",
     "yarn": "^1.10.0"
   }
-}
\ No newline at end of file
+}
Index: yarn.lock
<+>UTF-8
===================================================================
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock (revision Staged)
+++ b/yarn.lock (date 1665746037682)
@@ -999,6 +999,27 @@
   resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87"
   integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA==
 
+"@cubejs-client/core@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.31.0.tgz#ee507a3841962a63b255e19f1575e66fd1c726ee"
+  integrity sha512-72+7dVsLURGEYR0FCImf1XPLnc849zKKYQPDk0O/tHShX+8+k8ARt5VLadCl61mbBHIZZgH3OFzvqSREQm+Stw==
+  dependencies:
+    core-js "^3.6.5"
+    cross-fetch "^3.0.2"
+    dayjs "^1.10.4"
+    ramda "^0.27.0"
+    url-search-params-polyfill "^7.0.0"
+    uuid "^8.3.2"
+
+"@cubejs-client/vue@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.31.0.tgz#83db7d0ae153d12e2018faaa5b1dec00e02b8a57"
+  integrity sha512-7dm1yKFgQYx50BWnu6DPFr+bBFgnsyAZ0h1skrevtYU+s0kDBiwq1UQ45AZh/Q5w+AageenCh6N7bcn1AvVK8w==
+  dependencies:
+    "@cubejs-client/core" "^0.31.0"
+    core-js "^3.6.5"
+    ramda "^0.27.0"
+
 "@discoveryjs/json-ext@^0.5.0":
   version "0.5.6"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f"
@@ -3837,7 +3858,7 @@
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
   integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
 
-core-js@^3.25.5:
+core-js@^3.25.5, core-js@^3.6.5:
   version "3.25.5"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.5.tgz#e86f651a2ca8a0237a5f064c2fe56cef89646e27"
   integrity sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw==
@@ -3922,6 +3943,13 @@
   dependencies:
     jquery ">= 1.9.1"
 
+cross-fetch@^3.0.2:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+  dependencies:
+    node-fetch "2.6.7"
+
 cross-spawn@^6.0.5:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -4620,6 +4648,11 @@
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-5.0.1.tgz#60a27a2deb339f888ba4532f533e25ac73ca3d19"
   integrity sha512-DrcKxOW2am3mtqoJwBTK3OlWcF0QSk1p8diEWwpu3Mf//VdURD7XVaeOV738JvcaBiFfm9o2fisoMhiJH0aYxg==
 
+dayjs@^1.10.4:
+  version "1.11.5"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
+  integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
+
 de-indent@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -9192,7 +9225,7 @@
   resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
   integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=
 
-node-fetch@^2.6.1, node-fetch@^2.6.7:
+node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7:
   version "2.6.7"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
   integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@@ -10225,6 +10258,11 @@
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
 
+ramda@^0.27.0:
+  version "0.27.2"
+  resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1"
+  integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==
+
 randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -12035,6 +12073,11 @@
     mime-types "^2.1.27"
     schema-utils "^3.0.0"
 
+url-search-params-polyfill@^7.0.0:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-7.0.1.tgz#b900cd9a0d9d2ff757d500135256f2344879cbff"
+  integrity sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==
+
 url@^0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #370556 (closed)

Edited by Robert Hunt

Merge request reports