diff --git a/.yo-rc.json b/.yo-rc.json index 9f74cd6..23a2795 100644 --- a/.yo-rc.json +++ b/.yo-rc.json @@ -2,7 +2,6 @@ "generator-phovea": { "type": "lib-slib", "modules": [ - "tdp_core", "tdp_core" ], "libraries": [], @@ -21,4 +20,4 @@ "ignores": [], "extensions": [] } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 0447df9..eeafc4e 100644 --- a/README.md +++ b/README.md @@ -6,30 +6,35 @@ Matomo tracking for TDP applications based on provenance graph commands. Configuration ------------ -The tracking starts when a URL to a Matomo backend is set in the `config.js`. -The site ID corresponds with the Matomo site. +* The tracking starts when a URL to a Matomo backend is set in the `config.js`. +* The site ID corresponds with the Matomo site. +* Enable the [md5](https://en.wikipedia.org/wiki/MD5) encryption of user names to prevent plaintext logging (e.g., when using Matomo with LDAP login) ```js { "matomo": { "url": "https://matomo.my-example-domain.com/", // matomo url with a trailing slash - "site": "1" + "site": "1", + "encryptUserName": false } } ``` ### Provenance Commands -The tracked default provenance commands from [tdp_core](https://github.com/datavisyn/tdp_core) are defined in [actions.ts](./src/actions.ts). - -Add a list of custom events when initializing the tracking: +Provenance commands using the extension point `actionFunction` must be annotated with the property `tdp_matomo` in order to be found and tracked. +The `tdp_matomo` configuration property requires the properties `category` and `action` from the `IMatomoEvent` (in *src/matomo.ts*), which can contain arbitrary strings. ```ts -const trackableActions: ITrackableAction[] = [ - // id = phovea extension id - {id: 'targidCreateView', event: {category:'view', action: 'create'}}, -]; -trackApp(app, trackableActions); + registry.push('actionFunction', 'targidCreateView', function() { + return System.import('./internal/cmds'); + }, { + factory: 'createViewImpl', + tdp_matomo: { + category: 'view', + action: 'create' + } + }); ``` diff --git a/package.json b/package.json index cf47045..cc461d4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tdp_matomo", "description": "Matomo tracking for TDP applications", "homepage": "https://datavisyn.io", - "version": "1.0.0", + "version": "2.0.0", "author": { "name": "datavisyn GmbH", "email": "contact@datavisyn.io", @@ -108,6 +108,7 @@ "ts-loader": "4.0.1" }, "dependencies": { - "tdp_core": "github:datavisyn/tdp_core#semver:^5.2.0" + "crypto-js": "^3.1.9-1", + "tdp_core": "github:datavisyn/tdp_core#semver:^5.5.0" } } diff --git a/phovea.js b/phovea.js deleted file mode 100644 index 62a530e..0000000 --- a/phovea.js +++ /dev/null @@ -1,16 +0,0 @@ -/* ***************************************************************************** - * Caleydo - Visualization for Molecular Biology - http://caleydo.org - * Copyright (c) The Caleydo Team. All rights reserved. - * Licensed under the new BSD license, available at http://caleydo.org/license - **************************************************************************** */ - -//register all extensions in the registry following the given pattern -module.exports = function(registry) { - /// #if include('extension-type', 'extension-id') - //registry.push('extension-type', 'extension-id', function() { return import('./src/extension_impl'); }, {}); - /// #endif - // generator-phovea:begin - - // generator-phovea:end -}; - diff --git a/phovea_registry.js b/phovea_registry.js index ecac313..51a7327 100644 --- a/phovea_registry.js +++ b/phovea_registry.js @@ -5,12 +5,12 @@ **************************************************************************** */ import {register} from 'phovea_core/src/plugin'; - +import reg from './src/phovea'; /** * build a registry by registering all phovea modules */ //other modules import 'tdp_core/phovea_registry.js'; -import 'tdp_core/phovea_registry.js'; + //self -register('tdp_matomo',require('./phovea.js')); +register('tdp_matomo', reg); diff --git a/requirements.txt b/requirements.txt index 2888ae5..760572d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ --e git+https://github.com/datavisyn/tdp_core.git@develop#egg=tdp_core \ No newline at end of file +-e git+https://github.com/datavisyn/tdp_core.git@v5.5.0#egg=tdp_core \ No newline at end of file diff --git a/setup.py b/setup.py index d15dda2..9ed9b79 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python ############################################################################### # Caleydo - Visualization for Molecular Biology - http://caleydo.org # Copyright (c) The Caleydo Team. All rights reserved. diff --git a/src/actions.ts b/src/actions.ts deleted file mode 100644 index e5b7a70..0000000 --- a/src/actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {IMatomoEvent} from './matomo'; - -/** - * Map of trackable actions - * key = phovea extension id - */ -export const trackableActions = new Map(); - -// custom event -trackableActions.set('runChain', {category:'provenance', action: 'runChain'}); - -// from tdp_core\src\internal\cmds.ts -trackableActions.set('tdpInitSession', {category:'session', action: 'init'}); -trackableActions.set('tdpSetParameter', {category:'view', action: 'setParameter'}); - -// from tdp_core\src\lineup\internal\scorecmds.ts -trackableActions.set('tdpAddScore', {category:'score', action: 'add'}); -trackableActions.set('tdpRemoveScore', {category:'score', action: 'remove'}); - -// from tdp_core\src\lineup\internal\cmds.ts -trackableActions.set('lineupSetRankingSortCriteria', {category:'lineup', action: 'setRankingSortCriteria'}); -trackableActions.set('lineupSetSortCriteria', {category:'lineup', action: 'setRankingSortCriteria'}); -trackableActions.set('lineupSetGroupCriteria', {category:'lineup', action: 'setGroupCriteria'}); -trackableActions.set('lineupAddRanking', {category:'lineup', action: 'addRanking'}); -trackableActions.set('lineupSetColumn', {category:'lineup', action: 'setColumn'}); -trackableActions.set('lineupAddColumn', {category:'lineup', action: 'addColumn'}); -trackableActions.set('lineupMoveColumn', {category:'lineup', action: 'moveColumn'}); diff --git a/src/matomo.ts b/src/matomo.ts index bb344a6..63b6d29 100644 --- a/src/matomo.ts +++ b/src/matomo.ts @@ -1,9 +1,8 @@ -import {on} from 'phovea_core/src/event'; -import {GLOBAL_EVENT_USER_LOGGED_IN, IUser, GLOBAL_EVENT_USER_LOGGED_OUT} from 'phovea_core/src/security'; +import {IUser} from 'phovea_core/src/security'; import {ProvenanceGraph, ActionNode} from 'phovea_core/src/provenance'; import {getAPIJSON} from 'phovea_core/src/ajax'; -import ATDPApplication from 'tdp_core/src/ATDPApplication'; -import {trackableActions} from './actions'; +import {list} from 'phovea_core/src/plugin'; +import md5 from 'crypto-js/md5'; /** * Trackable Matomo event @@ -44,9 +43,44 @@ interface IPhoveaMatomoConfig { * ID of the Matomo site (generated when creating a page) */ site: string; + + /** + * Flag whether the user name should be encrypted using MD5 or not + */ + encryptUserName?: boolean; } -const matomo = { +class Matomo { + + private userId: string; + + init(config: IPhoveaMatomoConfig) { + if (!config.url) { + return false; + } + + const userId = (config.encryptUserName === true) ? md5(this.userId).toString() : this.userId; + + _paq.push(['setUserId', userId]); + + // _paq.push(['requireConsent']); TODO user consent form with opt out + _paq.push(['trackPageView']); + _paq.push(['enableLinkTracking']); + // enable correct measuring of the site since it is a single page site + _paq.push(['enableHeartBeatTimer']); + + _paq.push(['setTrackerUrl', `${config.url}matomo.php`]); + _paq.push(['setSiteId', config.site]); + + const s = document.createElement('script'); + s.type = 'text/javascript'; + s.async = true; + s.defer = true; + s.src = `${config.url}matomo.js`; + const base = document.getElementsByTagName('script')[0]; + base.insertAdjacentElement('beforebegin', s); + } + trackEvent(category: string, action: string, name?: string, value?: number) { const t: any[] = ['trackEvent', category, action]; if (typeof name === 'string') { @@ -56,22 +90,53 @@ const matomo = { t.push(value); } _paq.push(t); - }, - login(user: string) { - _paq.push(['setUserId', user]); - // _paq.push(['requireConsent']); TODO user consent form with opt out - _paq.push(['trackPageView']); - _paq.push(['enableLinkTracking']); - // enable correct measuring of the site since it is a single page site - _paq.push(['enableHeartBeatTimer']); - }, + } + + login(userId: string) { + // store for later as we need to wait for the config to know whether the user name should be encrypted or not + this.userId = userId; + } + logout() { _paq.push(['resetUserId']); _paq.push(['trackPageView']); } -}; +} + +const matomo = new Matomo(); + +/** + * Login extension point + */ +export function trackLogin(user: IUser) { + matomo.login(user.name); +} + +/** + * Logout extension point + */ +export function trackLogout() { + matomo.logout(); +} + +/** + * Provenance graph extension point + * @param graph ProvenanceGraph + */ +export async function trackProvenance(graph: ProvenanceGraph) { + if (graph.isEmpty) { + matomo.trackEvent('session', 'new', 'New Session'); + } else { + matomo.trackEvent('session', 'continue', `${graph.desc.id} at state ${Math.max(...graph.states.map((s) => s.id))}`); + } + + const trackableActions = new Map(); + + // load all registered actionFunction extension points and look if they contain a `analytics` property + list((desc) => desc.type === 'actionFunction' && desc.analytics).forEach((desc) => { + trackableActions.set(desc.id, desc.analytics); + }); -function trackGraph(graph: ProvenanceGraph, trackableActions: Map) { graph.on('execute', (_, node: ActionNode) => { if(!Array.from(trackableActions.keys()).includes(node.getAttr('f_id'))) { return; @@ -84,73 +149,11 @@ function trackGraph(graph: ProvenanceGraph, trackableActions: Map { - const event = trackableActions.get('runChain'); - matomo.trackEvent(event.category, event.action, 'Run actions in chain', nodes.length); - }); -} - -function initMamoto(config: IPhoveaMatomoConfig): boolean { - if (!config.url) { - return false; - } - _paq.push(['setTrackerUrl', `${config.url}matomo.php`]); - _paq.push(['setSiteId', config.site]); - - const s = document.createElement('script'); - s.type = 'text/javascript'; - s.async = true; - s.defer = true; - s.src = `${config.url}matomo.js`; - const base = document.getElementsByTagName('script')[0]; - base.insertAdjacentElement('beforebegin', s); - return true; -} -/** - * Track provenance commands of any TDPApplication - * - * Add custom actions using the phovea extension id: - * - * ```ts - * // id = phovea extension id - * const trackableActions: ITrackableAction[] = [ - * {id: 'targidCreateView', event: {category:'view', action: 'create'}}, - * ]; - * trackApp(app, trackableActions); - * ``` - * - * @param tdpApp ATDPApplication - * @param customActions List of custom actions - */ -export function trackApp(tdpApp: ATDPApplication, customActions?: {id: string, event: IMatomoEvent}[]): Promise { - // merge custom actions into trackable actions - if(customActions && customActions.length > 0) { - customActions.forEach((action) => trackableActions.set(action.id, action.event)); - } - - const matomoConfig = getAPIJSON('/tdp/config/matomo'); - - tdpApp.on(ATDPApplication.EVENT_OPEN_START_MENU, () => matomo.trackEvent('startMenu', 'open', 'Open start menu')); - - on(GLOBAL_EVENT_USER_LOGGED_IN, (_, user: IUser) => { - matomo.login(user.name); - tdpApp.graph.then((graph) => { - if (graph.isEmpty) { - matomo.trackEvent('session', 'new', 'New Session'); - } else { - matomo.trackEvent('session', 'continue', `${graph.desc.id} at state ${tdpApp.clueManager.storedState || Math.max(...graph.states.map((s) => s.id))}`); - } - - matomoConfig.then((config: IPhoveaMatomoConfig) => { - trackGraph(graph, trackableActions); - }); - }); - }); - - on(GLOBAL_EVENT_USER_LOGGED_OUT, () => { - matomo.logout(); + graph.on('run_chain', (_, nodes: ActionNode[]) => { + matomo.trackEvent('provenance', 'runChain', 'Run actions in chain', nodes.length); }); - return matomoConfig.then((config: IPhoveaMatomoConfig) => initMamoto(config)); + const config: IPhoveaMatomoConfig = await getAPIJSON('/tdp/config/matomo'); + matomo.init(config); } diff --git a/src/phovea.ts b/src/phovea.ts new file mode 100644 index 0000000..bf56e3c --- /dev/null +++ b/src/phovea.ts @@ -0,0 +1,26 @@ +/* ***************************************************************************** + * Caleydo - Visualization for Molecular Biology - http://caleydo.org + * Copyright (c) The Caleydo Team. All rights reserved. + * Licensed under the new BSD license, available at http://caleydo.org/license + **************************************************************************** */ +import {IRegistry} from 'phovea_core/src/plugin'; + +// TODO: Use these constants for extension types below. Waiting for `tdp_core` to point to `phovea_clue` develop branch again (see https://github.com/datavisyn/tdp_core/blob/develop/package.json#L82). +// import {EP_PHOVEA_CORE_LOGIN, EP_PHOVEA_CORE_LOGOUT} from 'phovea_core/src/extensions'; +// import {EP_PHOVEA_CLUE_PROVENANCE_GRAPH} from 'phovea_clue/src/extensions'; + +export default function (registry: IRegistry) { + + registry.push('epPhoveaCoreLogin', 'matomoLogin', () => System.import('./matomo'), { + factory: 'trackLogin' + }); + + registry.push('epPhoveaCoreLogout', 'matomoLogout', () => System.import('./matomo'), { + factory: 'trackLogout' + }); + + registry.push('epPhoveaClueProvenanceGraph', 'matomoAnalytics', () => System.import('./matomo'), { + factory: 'trackProvenance' + }); + +} diff --git a/tdp_matomo/config.json b/tdp_matomo/config.json index 3b13b59..f0bd0a8 100644 --- a/tdp_matomo/config.json +++ b/tdp_matomo/config.json @@ -1,6 +1,7 @@ { "matomo": { "url": "", // matomo url with a trailing slash - "site": "1" + "site": "1", + "encryptUserName": false } } diff --git a/tests/index.test.ts b/tests/index.test.ts index 99fb020..0996b04 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,8 +1,16 @@ /// -import {trackApp} from '../src/index'; +import {trackProvenance, trackLogin, trackLogout} from '../src/index'; describe('index', () => { - it('trackApp() exists', () => { - expect(typeof trackApp).toBe('function'); + it('trackLogin() exists', () => { + expect(typeof trackLogin).toBe('function'); + }); + + it('trackLogout() exists', () => { + expect(typeof trackLogout).toBe('function'); + }); + + it('trackProvenance() exists', () => { + expect(typeof trackProvenance).toBe('function'); }); });