Skip to content

Commit ad4a4e9

Browse files
committed
NXT-4374: setup analytics service
NXT-4374 (Instrumentation abstraction in modern UI)
1 parent e893a04 commit ad4a4e9

File tree

7 files changed

+374
-5
lines changed

7 files changed

+374
-5
lines changed

org.knime.ui.js/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@fontsource/roboto": "4.x",
4040
"@fontsource/roboto-condensed": "4.x",
4141
"@fontsource/roboto-mono": "4.x",
42+
"@gtm-support/vue-gtm": "^3.1.0",
4243
"@knime/components": "1.42.1",
4344
"@knime/hub-features": "^1.15.5",
4445
"@knime/kds-components": "^0.5.2",

org.knime.ui.js/pnpm-lock.yaml

Lines changed: 39 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type App } from "vue";
2+
3+
import { matomoAdapter } from "./matomo-adapter";
4+
import type { AnalyticsConfig, AnalyticsService } from "./types";
5+
6+
const noop = (...args: any[]) => {
7+
if (import.meta.env.DEV) {
8+
consola.trace("Analytics::DEV", ...args);
9+
}
10+
};
11+
12+
const noopService: AnalyticsService = {
13+
track: noop,
14+
};
15+
16+
let __analyticsService: AnalyticsService = noopService;
17+
18+
export const createAnalyticsService = async (
19+
app: App,
20+
config: AnalyticsConfig,
21+
): Promise<void> => {
22+
// dummy async in case it's needed later
23+
await new Promise((r) => setTimeout(r, 0));
24+
25+
matomoAdapter.init(app, config);
26+
27+
__analyticsService = {
28+
track: matomoAdapter.track,
29+
};
30+
};
31+
32+
export const useAnalyticsService = () => ({ track: __analyticsService.track });
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/* eslint-disable func-style */
2+
import type { App } from "vue";
3+
import { createGtm } from "@gtm-support/vue-gtm";
4+
5+
import {
6+
ANALYTIC_EVENT_CATEGORIES,
7+
type AnalyticEventNames,
8+
type AnalyticEvents,
9+
type AnalyticsConfig,
10+
type TrackFn,
11+
} from "./types";
12+
13+
// Following types are taken from:
14+
// https://github.com/SocialGouv/matomo-next/blob/master/src/types.ts
15+
16+
type HeatmapConfig = {
17+
/**
18+
* Enable/disable keystroke capture (default: false)
19+
* Since v3.2.0, keystrokes are disabled by default
20+
*/
21+
captureKeystrokes?: boolean;
22+
23+
/**
24+
* Enable/disable recording of mouse and touch movements (default: true)
25+
* Set to false to disable the "Move Heatmap" feature
26+
*/
27+
recordMovements?: boolean;
28+
29+
/**
30+
* Maximum capture time in seconds (default: 600 = 10 minutes)
31+
* Set to less than 29 minutes to avoid creating new visits
32+
*/
33+
maxCaptureTime?: number;
34+
35+
/**
36+
* Disable automatic detection of new page views (default: false)
37+
* Set to true if you track "virtual" page views for events/downloads
38+
*/
39+
disableAutoDetectNewPageView?: boolean;
40+
41+
/**
42+
* Custom trigger function to control when recording happens
43+
* Return true to record, false to skip
44+
* @param config - Configuration object with heatmap/session ID
45+
*/
46+
trigger?: (config: { id?: number }) => boolean;
47+
48+
/**
49+
* Manually add heatmap/session configuration
50+
* Use this to manually configure specific heatmaps or sessions
51+
*/
52+
addConfig?: {
53+
heatmap?: { id: number };
54+
sessionRecording?: { id: number };
55+
};
56+
};
57+
58+
/**
59+
* Custom Dimensions object that can be passed as the last argument of many tracking calls
60+
* (action-scoped dimensions).
61+
*
62+
* Examples:
63+
* - `["trackEvent", "Video", "Play", "Intro", 42, { dimension1: "Premium" }]`
64+
* - `["trackSiteSearch", "keyword", "category", 12, { dimension4: "Test" }]`
65+
* - `["trackPageView", "My title", { dimension7: "Value" }]`
66+
*
67+
* Note: keys are expected to be `"dimension1"`, `"dimension2"`, etc.
68+
* We intentionally keep this type compatible with older TS/ESLint parsers.
69+
*/
70+
type Dimensions = {
71+
dimension1?: string;
72+
dimension2?: string;
73+
dimension3?: string;
74+
dimension4?: string;
75+
dimension5?: string;
76+
dimension6?: string;
77+
dimension7?: string;
78+
dimension8?: string;
79+
dimension9?: string;
80+
dimension10?: string;
81+
};
82+
83+
/**
84+
* A single value inside a Matomo command pushed to the queue.
85+
* Kept as a separate exported type for consumers that want to model custom commands.
86+
*/
87+
export type PushArg =
88+
| string
89+
| number
90+
| boolean
91+
| null
92+
| undefined
93+
| Record<string, unknown>
94+
| readonly unknown[]
95+
| ((...args: any[]) => unknown);
96+
97+
/**
98+
* Strict Matomo `trackEvent` typing.
99+
*
100+
* Log an event with an event category (Videos, Music, Games...), an event action
101+
* (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), and an optional event name
102+
*
103+
* Notes:
104+
* - `name` and `value` are optional
105+
* - `value` (when present) must be numeric
106+
* - `value` requires `name` to be provided (no "hole" argument)
107+
*/
108+
type MatomoTrackEventCommand =
109+
| readonly ["trackEvent", string, string]
110+
| readonly ["trackEvent", string, string, Dimensions]
111+
| readonly ["trackEvent", string, string, string]
112+
| readonly ["trackEvent", string, string, string, Dimensions]
113+
| readonly ["trackEvent", string, string, string, number]
114+
| readonly ["trackEvent", string, string, string, number, Dimensions];
115+
116+
/**
117+
* Core commands used by this library (and/or documented in docs).
118+
* This list is intentionally limited: any unknown command is still allowed via `MatomoCustomCommand`.
119+
*/
120+
type MatomoCoreCommand =
121+
// Page view
122+
| readonly ["trackPageView"]
123+
| readonly ["trackPageView", string]
124+
| readonly ["trackPageView", Dimensions]
125+
| readonly ["trackPageView", string, Dimensions]
126+
// Standard setup / configuration
127+
| readonly ["enableLinkTracking"]
128+
| readonly ["disableCookies"]
129+
| readonly ["setTrackerUrl", string]
130+
| readonly ["setSiteId", string]
131+
| readonly ["setReferrerUrl", string]
132+
| readonly ["setCustomUrl", string]
133+
| readonly ["deleteCustomVariables", string]
134+
| readonly ["setDocumentTitle", string]
135+
// Site search (Matomo supports an optional dimensions object as last param)
136+
| readonly ["trackSiteSearch", string]
137+
| readonly ["trackSiteSearch", string, Dimensions]
138+
| readonly ["trackSiteSearch", string, string]
139+
| readonly ["trackSiteSearch", string, string, Dimensions]
140+
| readonly ["trackSiteSearch", string, string, number]
141+
| readonly ["trackSiteSearch", string, string, number, Dimensions]
142+
// Heartbeat
143+
| readonly ["enableHeartBeatTimer"]
144+
| readonly ["enableHeartBeatTimer", number]
145+
// Custom dimensions (global, persisted until changed)
146+
| readonly ["setCustomDimension", number, string]
147+
// Goals (Matomo supports an optional dimensions object as last param)
148+
| readonly ["trackGoal", number]
149+
| readonly ["trackGoal", number, Dimensions]
150+
| readonly ["trackGoal", number, number]
151+
| readonly ["trackGoal", number, number, Dimensions]
152+
// Links (Matomo supports an optional dimensions object as last param)
153+
| readonly ["trackLink", string, string]
154+
| readonly ["trackLink", string, string, Dimensions]
155+
// User
156+
| readonly ["setUserId", string];
157+
158+
/**
159+
* Heatmap & Session Recording plugin commands used by this library.
160+
*/
161+
type HeatmapSessionRecordingCommand =
162+
| readonly ["HeatmapSessionRecording::enableDebugMode"]
163+
| readonly ["HeatmapSessionRecording::disableCaptureKeystrokes"]
164+
| readonly ["HeatmapSessionRecording::disableRecordMovements"]
165+
| readonly ["HeatmapSessionRecording::setMaxCaptureTime", number]
166+
| readonly ["HeatmapSessionRecording::disableAutoDetectNewPageView"]
167+
| readonly [
168+
"HeatmapSessionRecording::setTrigger",
169+
NonNullable<HeatmapConfig["trigger"]>,
170+
]
171+
| readonly [
172+
"HeatmapSessionRecording::addConfig",
173+
NonNullable<HeatmapConfig["addConfig"]>,
174+
]
175+
| readonly ["HeatmapSessionRecording::enable"];
176+
177+
type MatomoKnownCommand =
178+
| MatomoTrackEventCommand
179+
| MatomoCoreCommand
180+
| HeatmapSessionRecordingCommand;
181+
182+
/**
183+
* Matomo also supports queueing functions executed once the tracker is ready.
184+
*/
185+
type MatomoCallbackCommand = readonly [(...args: any[]) => unknown];
186+
187+
export type PushArgs = MatomoKnownCommand | MatomoCallbackCommand;
188+
189+
function push(args: PushArgs): void {
190+
if (!(window as any)._paq) {
191+
(window as any)._paq = [];
192+
}
193+
194+
(window as any)._paq.push(args);
195+
}
196+
197+
type Mapper = {
198+
[K in AnalyticEventNames]: (payload: AnalyticEvents[K]) => void;
199+
};
200+
201+
const mapper: Mapper = {
202+
"node.added": (payload) => {
203+
push(["trackEvent", ANALYTIC_EVENT_CATEGORIES.Authoring, payload.via, ""]);
204+
},
205+
"node.executed": (payload) => {
206+
push(["trackEvent", ANALYTIC_EVENT_CATEGORIES.Execution, payload.via, ""]);
207+
},
208+
};
209+
210+
const track: TrackFn = (eventType, payload) => {
211+
mapper[eventType](payload);
212+
};
213+
214+
const init = (app: App, config: AnalyticsConfig) => {
215+
// TODO: investigate
216+
/**
217+
* We use Matomo, but indirectly. Currently we fetch the analytics scripts
218+
* via google-tag-manager (gtm). However, the actual tracking tool used under the hood
219+
* is Matomo. We would need to check if the gtm integration can be used directly, but for this
220+
* impl, we just use gtm for init and then further tracking is made via the Matomo
221+
* global functions
222+
*/
223+
224+
const plugin = createGtm({ id: config.trackingAPIKey, defer: true });
225+
app.use(plugin);
226+
227+
// eslint-disable-next-line no-magic-numbers
228+
push(["enableHeartBeatTimer", 30]);
229+
};
230+
231+
export const matomoAdapter = { init, track };
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type AnalyticEvents = {
2+
"node.added": {
3+
nodeId: string;
4+
nodeType: string;
5+
via:
6+
| "node-repository-drag"
7+
| "node-repository-dbl-click"
8+
| "clipboard"
9+
| "quick-action";
10+
};
11+
"node.executed": {
12+
nodeId: string;
13+
nodeType: string;
14+
via: "actionbar" | "shortcut" | "context-menu";
15+
};
16+
// ...TODO: more TBD
17+
};
18+
19+
export const ANALYTIC_EVENT_CATEGORIES = {
20+
Authoring: "Authoring",
21+
Execution: "Execution",
22+
// ...TODO: more TBD
23+
} as const;
24+
25+
export type AnalyticEventNames = keyof AnalyticEvents;
26+
27+
export type TrackFn = <K extends AnalyticEventNames>(
28+
type: K,
29+
payload: AnalyticEvents[K],
30+
) => void;
31+
32+
export type AnalyticsConfig = {
33+
trackingAPIKey: string;
34+
context: {
35+
userId: string;
36+
sessionId: string;
37+
jobId: string;
38+
};
39+
};
40+
41+
export interface AnalyticsService {
42+
track: TrackFn;
43+
}

0 commit comments

Comments
 (0)