diff --git a/.eslintrc.yml b/.eslintrc.yml index 80295fbb1..5ccead36c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -79,6 +79,7 @@ overrides: react/forbid-prop-types: off react/require-default-props: off react/no-array-index-key: off + no-underscore-dangle: off react/jsx-filename-extension: [0] import/no-extraneous-dependencies: ['error', { devDependencies: true }] '@typescript-eslint/explicit-module-boundary-types': off diff --git a/app/angulartics.matomo/analytics-services.ts b/app/angulartics.matomo/analytics-services.ts new file mode 100644 index 000000000..a1f335ce2 --- /dev/null +++ b/app/angulartics.matomo/analytics-services.ts @@ -0,0 +1,113 @@ +import _ from 'lodash-es'; + +const categories = [ + 'docker', + 'kubernetes', + 'aci', + 'portainer', + 'edge', +] as const; +type Category = typeof categories[number]; + +enum DimensionConfig { + PortainerVersion = 1, + PortainerInstanceID, + PortainerUserRole, + PortainerEndpointUserRole, +} + +interface TrackEventProps { + category: Category; + metadata?: Record; + value?: string | number; + dimensions?: DimensionConfig; +} + +export function setPortainerStatus(instanceID: string, version: string) { + setCustomDimension(DimensionConfig.PortainerInstanceID, instanceID); + setCustomDimension(DimensionConfig.PortainerVersion, version); +} + +export function setUserRole(role: string) { + setCustomDimension(DimensionConfig.PortainerUserRole, role); +} + +export function clearUserRole() { + deleteCustomDimension(DimensionConfig.PortainerUserRole); +} + +export function setUserEndpointRole(role: string) { + setCustomDimension(DimensionConfig.PortainerEndpointUserRole, role); +} + +export function clearUserEndpointRole() { + deleteCustomDimension(DimensionConfig.PortainerEndpointUserRole); +} + +function setCustomDimension(dimensionId: number, value: string) { + push('setCustomDimension', dimensionId, value); +} + +function deleteCustomDimension(dimensionId: number) { + push('deleteCustomDimension', dimensionId.toString()); +} + +export function push( + name: string, + ...args: (string | number | DimensionConfig)[] +) { + if (typeof window !== 'undefined') { + window._paq.push([name, ...args]); + } +} + +export function trackEvent(action: string, properties: TrackEventProps) { + /** + * @description Logs an event with an event category (Videos, Music, Games...), an event + * action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), and an optional + * event name and optional numeric value. + * + * @link https://piwik.org/docs/event-tracking/ + * @link https://developer.piwik.org/api-reference/tracking-javascript#using-the-tracker-object + * + */ + + let { value } = properties; + const { metadata, dimensions, category } = properties; + // PAQ requires that eventValue be an integer, see: http://piwik.org/docs/event-tracking + if (value) { + const parsed = parseInt(value.toString(), 10); + value = Number.isNaN(parsed) ? 0 : parsed; + } + + if (!category) { + throw new Error('missing category'); + } + + if (!categories.includes(category)) { + throw new Error('unsupported category'); + } + + let metadataString = ''; + if (metadata) { + const kebabCasedMetadata = Object.fromEntries( + Object.entries(metadata).map(([key, value]) => [_.kebabCase(key), value]) + ); + metadataString = JSON.stringify(kebabCasedMetadata).toLowerCase(); + } + + push( + 'trackEvent', + category, + action.toLowerCase(), + metadataString, // Changed in favour of Piwik documentation. Added fallback so it's backwards compatible. + value || '', + dimensions || {} + ); +} + +declare global { + interface Window { + _paq: [string, ...(string | number)[]][]; + } +} diff --git a/app/angulartics.matomo/index.js b/app/angulartics.matomo/index.js index f6a4e528e..2ba89f610 100644 --- a/app/angulartics.matomo/index.js +++ b/app/angulartics.matomo/index.js @@ -1,17 +1,7 @@ import angular from 'angular'; -import _ from 'lodash-es'; - +import { setPortainerStatus, setUserRole, clearUserRole, setUserEndpointRole, clearUserEndpointRole, push, trackEvent } from './analytics-services'; const basePath = 'http://portainer-ce.app'; -const dimensions = { - PortainerVersion: 1, - PortainerInstanceID: 2, - PortainerUserRole: 3, - PortainerEndpointUserRole: 4, -}; - -const categories = ['docker', 'kubernetes', 'aci', 'portainer', 'edge']; - // forked from https://github.com/angulartics/angulartics-piwik/blob/master/src/angulartics-piwik.js /** @@ -37,12 +27,12 @@ function config($analyticsProvider, $windowProvider) { // scope: visit or page. Defaults to 'page' $analyticsProvider.api.setCustomVariable = function (varIndex, varName, value, scope = 'page') { - push(['setCustomVariable', varIndex, varName, value, scope]); + push('setCustomVariable', varIndex, varName, value, scope); }; // scope: visit or page. Defaults to 'page' $analyticsProvider.api.deleteCustomVariable = function (varIndex, scope = 'page') { - $window._paq.push(['deleteCustomVariable', varIndex, scope]); + push('deleteCustomVariable', varIndex, scope); }; // trackSiteSearch(keyword, category, [searchCount]) @@ -63,7 +53,7 @@ function config($analyticsProvider, $windowProvider) { // logs a conversion for goal 1. revenue is optional // trackGoal(goalID, [revenue]); $analyticsProvider.api.trackGoal = function (goalID, revenue) { - push(['trackGoal', goalID, revenue || 0]); + push('trackGoal', goalID, revenue || 0); }; // track outlink or download @@ -71,21 +61,21 @@ function config($analyticsProvider, $windowProvider) { // trackLink(url, [linkType]); $analyticsProvider.api.trackLink = function (url, linkType) { const type = linkType || 'link'; - push(['trackLink', url, type]); + push('trackLink', url, type); }; // Set default angulartics page and event tracking $analyticsProvider.registerSetUsername(function (username) { - push(['setUserId', username]); + push('setUserId', username); }); // locationObj is the angular $location object $analyticsProvider.registerPageTrack(function (path) { - push(['setDocumentTitle', $window.document.title]); - push(['setReferrerUrl', '']); - push(['setCustomUrl', basePath + path]); - push(['trackPageView']); + push('setDocumentTitle', $window.document.title); + push('setReferrerUrl', ''); + push('setCustomUrl', basePath + path); + push('trackPageView'); }); /** @@ -95,56 +85,7 @@ function config($analyticsProvider, $windowProvider) { * @param {string} action A string corresponding to the type of event that needs to be tracked. * @param {object} properties The properties that need to be logged with the event. */ - $analyticsProvider.registerEventTrack(function trackEvent(action, properties = {}) { - /** - * @description Logs an event with an event category (Videos, Music, Games...), an event - * action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), and an optional - * event name and optional numeric value. - * - * @link https://piwik.org/docs/event-tracking/ - * @link https://developer.piwik.org/api-reference/tracking-javascript#using-the-tracker-object - * - * @property {string} category - * @property {string} action - * @property {object} metadata - * @property value (optional) - * @property dimensions (optional) - */ - - let { category, metadata, value, dimensions } = properties; - - // PAQ requires that eventValue be an integer, see: http://piwik.org/docs/event-tracking - if (value) { - const parsed = parseInt(properties.value, 10); - properties.value = isNaN(parsed) ? 0 : parsed; - } - - if (!category) { - throw new Error('missing category'); - } - category = category.toLowerCase(); - - if (!categories.includes(category)) { - throw new Error('unsupported category'); - } - - action = action.toLowerCase(); - - let metadataString = ''; - if (metadata) { - const kebabCasedMetadata = Object.fromEntries(Object.entries(metadata).map(([key, value]) => [_.kebabCase(key), value])); - metadataString = JSON.stringify(kebabCasedMetadata).toLowerCase(); - } - - push([ - 'trackEvent', - category, - action, - metadataString, // Changed in favour of Piwik documentation. Added fallback so it's backwards compatible. - value, - dimensions || {}, - ]); - }); + $analyticsProvider.registerEventTrack(trackEvent); /** * @name exceptionTrack @@ -154,41 +95,6 @@ function config($analyticsProvider, $windowProvider) { * @param {object} cause The cause of the error given from $exceptionHandler, not used. */ $analyticsProvider.registerExceptionTrack(function (error) { - push(['trackEvent', 'Exceptions', error.toString(), error.stack, 0]); + push('trackEvent', 'Exceptions', error.toString(), error.stack, 0); }); - - function push(args) { - if ($window._paq) { - $window._paq.push(args); - } - } - - function setPortainerStatus(instanceID, version) { - setCustomDimension(dimensions.PortainerInstanceID, instanceID); - setCustomDimension(dimensions.PortainerVersion, version); - } - - function setUserRole(role) { - setCustomDimension(dimensions.PortainerUserRole, role); - } - - function clearUserRole() { - deleteCustomDimension(dimensions.PortainerUserRole); - } - - function setUserEndpointRole(role) { - setCustomDimension(dimensions.PortainerEndpointUserRole, role); - } - - function clearUserEndpointRole() { - deleteCustomDimension(dimensions.PortainerEndpointUserRole); - } - - function setCustomDimension(dimensionId, value) { - push(['setCustomDimension', dimensionId, value]); - } - - function deleteCustomDimension(dimensionId) { - push(['deleteCustomDimension', dimensionId]); - } }