@knime/hub-features 1.26.2 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @knime/hub-features
2
2
 
3
+ ## 1.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2908385: - split returned events based on event source (editor/cloudhome)
8
+ - add secret created event (cloudhome)
9
+ - introduce page property to interaction property (only for cloudhome events)
10
+
11
+ ### Patch Changes
12
+
13
+ - a2f988b: add cloudhome events for creating and sharing deployments
14
+
15
+ ## 1.27.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 3a49573: Rework API for analytic events and the underlying schema
20
+
3
21
  ## 1.26.2
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/hub-features",
3
- "version": "1.26.2",
3
+ "version": "1.28.0",
4
4
  "description": "Vue components & composables for shared hub features",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,8 +39,8 @@
39
39
  "lodash-es": "4.18.1",
40
40
  "ofetch": "^1.4.1",
41
41
  "typescript": "^5.9.3",
42
- "@knime/components": "1.46.6",
43
42
  "@knime/styles": "1.15.1",
43
+ "@knime/components": "1.46.6",
44
44
  "@knime/utils": "1.11.0"
45
45
  },
46
46
  "peerDependencies": {
@@ -62,6 +62,8 @@
62
62
  "scripts": {
63
63
  "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
64
64
  "test:unit": "TZ='Europe/Berlin' vitest",
65
- "generate-analytics-events": "json2ts ./src/analytics/schema/schema.json > ./src/analytics/schema/schema.d.ts"
65
+ "generate-analytics-events": "pnpm generate-event-types && pnpm generate-event-functions",
66
+ "generate-event-types": "json2ts ./src/analytics/schema/schema.json > ./src/analytics/schema/schema.d.ts",
67
+ "generate-event-functions": "node ./src/analytics/schema/code-generator.js"
66
68
  }
67
69
  }
@@ -2,86 +2,32 @@
2
2
 
3
3
  ## Preface
4
4
 
5
- This package provides helpers related to analytics events. It provides 2 main behaviors:
6
-
7
- - Provides a builder function that creates and normalizes event payloads
8
- - Provides a sender function that forwards a given event payload to a third party API.
9
- The implementation of the third-party API is up to the consumer as long as it satisfies the adapter contract
10
-
11
- The main reason why these behaviors are split are for increased flexibility and to also enable this feature being used on a setup where the creation of the event is separated to the sending of the event. Like for example, on host/guest application combination where there's an iframe communication layer between both apps.
5
+ This package provides functions that can produce analytics events.
12
6
 
13
7
  ## Usage examples
14
8
 
15
- ### Building event payloads
16
-
17
- As stated above, some applications might only care (or have the correct setup) to construct events based on different code paths and user interactions.
9
+ This package makes no assumption of how the events are sent. Therefore, one must supply a "sender" as
10
+ part of the initialization.
18
11
 
19
12
  ```ts
20
13
  import { analyticsEvents } from "@knime/hub-features/analytics";
21
14
 
22
- // Create a small factory bound to your context
23
- const { newEvent } = analyticsEvents.eventBuilder({ jobId: "job-123" });
24
-
25
- // Build an event. Event ids are strongly-typed, combining their special format
26
- // and validating payloads, if required
27
- const event = newEvent({ id: "kai_prompted::kaiqa_button_prompt" });
28
-
29
- // once you have this event object, the application can decide what to do with it.
30
- // Whether that is sending it through an iframe as a message or some other event bus. OR sending it directly if it's so desired (see Sending events section)
31
- ```
32
-
33
- ### Keeping types in-sync
34
-
35
- Due to the strict-type setup and the shape of the API of this package, reusing the type signature
36
- of the function that creates the events can be tricky. However, this is helpful, for example, if you want to
37
- wrap the create function with some other behavior of your own but keep the strong-type guarantees.
38
- Below you can see an example for this use-case:
39
-
40
- ```ts
41
- import {
42
- analyticsEvents,
43
- type CreateEventFn,
44
- } from "@knime/hub-features/analytics";
45
-
46
- const myCustomFunction: CreateEventFn = (...args) => {
47
- const { newEvent } = analyticsEvents.eventBuilder(TheNeededContext);
48
-
49
- // do something before
50
-
51
- const event = newEvent(...args);
52
-
53
- // do something with event
15
+ // Create a sender, which can be pretty much any object that provides a `send` function
16
+ const mySender = {
17
+ send: ({ event }) => {
18
+ // do something with event (e.g send over WS, dispatch to an iframe's parent, etc)
19
+ },
54
20
  };
55
- ```
56
-
57
- In this case, the type signature of `myCustomFunction` will match the one of `newEvent`, just like
58
- in the previous example.
59
-
60
- ### Sending events
61
-
62
- As stated in the preface, to send events, you need to provide an adapter
63
-
64
- ```ts
65
- import {
66
- analyticsEvents,
67
- type AnalyticsAdapter,
68
- } from "@knime/hub-features/analytics";
69
-
70
- const myAdapter: AnalyticsAdapter = {
71
- sendEvent({ event, metadata, idParser }) {
72
- console.log("Parsed id", idParser(event.id));
73
- console.log("Metadata", metadata);
74
- console.log("Event", event)
75
- }
76
- }
77
-
78
- // create the sender
79
- const sender = analyticsEvents.eventSender(myAdapter);
80
-
81
- // some event
82
- const { newEvent } = analyticsEvents.eventBuilder(TheNeededContext);
83
- const event = newEvent(...);
84
21
 
85
- // then...
86
- sender.send(event);
22
+ // Initialize the tracker
23
+ type SomeContext = { myContextField: string };
24
+ const eventTracker = analyticsEvents.init<SomeContext>({
25
+ context: { myContextField: "foo-123" },
26
+ sender: mySender,
27
+ });
28
+
29
+ // Track an event. `someEvent` here is a placeholder name. In reality, the API will provide named functions that
30
+ // strongly-typed and may or may not require payload depending on the event's definition. Under the hood this will
31
+ // use the configured sender to forward your event
32
+ eventTracker.someEvent();
87
33
  ```
@@ -1,76 +1,116 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import * as cloudHomeFunctions from "./schema/cloudhome-event-functions";
3
+ import * as editorFunctions from "./schema/editor-event-functions";
1
4
  import type {
2
- AnalyticsAdapter,
3
- AnalyticsPayload,
4
- Context,
5
- CreateEventFn,
5
+ AnalyticsEvent,
6
+ AnalyticsEventSender,
7
+ DefaultContext,
6
8
  Metadata,
7
9
  } from "./types";
8
- import { eventID } from "./utils/eventIds";
9
- import { toSnakeCaseDeep } from "./utils/toSnakeCaseDeep";
10
10
 
11
11
  const METADATA: Metadata = Object.freeze({
12
- version: "v1.0",
12
+ version: "v2.0",
13
13
  });
14
14
 
15
+ /**
16
+ * Unwraps the inner function imported from the generated functions.
17
+ * With this supply the context here and just expose the inner function
18
+ * but also ensure we keep the same names and type constraints
19
+ */
20
+ type UnwrapFunction<T> = T extends (ctx: DefaultContext) => infer R
21
+ ? R extends (...args: infer P) => infer Q
22
+ ? (...args: P) => Q
23
+ : never
24
+ : never;
25
+
26
+ type EditorFunctionMap = typeof editorFunctions;
27
+ type CloudHomeFunctionMap = typeof cloudHomeFunctions;
28
+
29
+ type EditorBuilderFunctions = {
30
+ [K in keyof EditorFunctionMap]: UnwrapFunction<EditorFunctionMap[K]>;
31
+ };
32
+ type CloudHomeBuilderFunctions = {
33
+ [K in keyof CloudHomeFunctionMap]: UnwrapFunction<CloudHomeFunctionMap[K]>;
34
+ };
35
+
36
+ type EditorSenderFunctions = {
37
+ [K in keyof EditorBuilderFunctions]: (
38
+ ...args: Parameters<EditorBuilderFunctions[K]>
39
+ ) => void;
40
+ };
41
+ type CloudHomeSenderFunctions = {
42
+ [K in keyof CloudHomeBuilderFunctions]: (
43
+ ...args: Parameters<CloudHomeBuilderFunctions[K]>
44
+ ) => void;
45
+ };
46
+
47
+ type BuilderFunctions<T extends DefaultContext> =
48
+ T["eventSource"] extends "editor"
49
+ ? EditorBuilderFunctions
50
+ : CloudHomeBuilderFunctions;
51
+
52
+ type SenderFunctions<T extends DefaultContext> =
53
+ T["eventSource"] extends "editor"
54
+ ? EditorSenderFunctions
55
+ : CloudHomeSenderFunctions;
56
+
57
+ /**
58
+ * This function creates an event builder, which lets you construct correct and strongly-typed
59
+ * event payloads according to the current schema used by this package
60
+ * @param context
61
+ * @returns
62
+ */
63
+ function eventBuilder<T extends DefaultContext>(
64
+ context: T,
65
+ ): BuilderFunctions<T> {
66
+ const unwrapped = Object.fromEntries(
67
+ Object.entries(
68
+ context.eventSource === "editor" ? editorFunctions : cloudHomeFunctions,
69
+ ).map(([name, fn]) => [name, fn(context)]),
70
+ ) as BuilderFunctions<T>;
71
+
72
+ return { ...unwrapped };
73
+ }
74
+
15
75
  export const analyticsEvents = Object.freeze({
16
76
  METADATA,
17
77
  /**
18
- * This function creates an event builder, which lets you construct correct and strongly-typed
19
- * event payloads according to the current schema used by this package
20
- * @param context
21
- * @returns
78
+ * Initialize the analytic event tracker
22
79
  */
23
- eventBuilder: (context: Context) => {
24
- const newEvent: CreateEventFn<AnalyticsPayload> = ({
25
- id: eventId,
26
- payload: eventData,
27
- }) => {
28
- // runtime check just in case - shouldn't be needed due to TS
29
- if (!eventID(eventId).isValid()) {
30
- throw new Error(`Implementation error: Invalid event id: ${eventId}`);
31
- }
80
+ init: <T extends DefaultContext>(params: {
81
+ /**
82
+ * Context for the initialized tracker. This will remain unchanged throughout the lifecycle
83
+ * of this tracker
84
+ */
85
+ context: T;
86
+ /**
87
+ * Sender interface; will be used to determine how to handle each event when its
88
+ * corresponding track function is invoked
89
+ */
90
+ sender: AnalyticsEventSender;
91
+ }) => {
92
+ const builder = eventBuilder(params.context);
32
93
 
33
- const uniqueEventId = crypto.randomUUID();
94
+ type AnyFn = (...args: any[]) => any;
34
95
 
35
- const data = (() => {
36
- if (!eventData) {
37
- return undefined;
38
- }
96
+ const wrapper = {} as SenderFunctions<T>;
39
97
 
40
- return toSnakeCaseDeep((eventData as unknown) ?? {});
41
- })();
98
+ // keep all the same functions of the builder but wrap around them for extra behavior
99
+ Object.keys(builder).forEach((fnName) => {
100
+ const key = fnName as keyof SenderFunctions<T>;
101
+ (wrapper as any)[key] = (...args: any[]) => {
102
+ // TS issue:
103
+ // ```A spread argument must either have a tuple type or be passed to a rest parameter.```
104
+ // This happens because when calling the methods generically, TS tries to join all function signatures
105
+ // into a union of tuples that contains all possible argument combinations for all of the functions.
106
+ // This leads to an unresolvable type error
107
+ // More details on the issue, see: https://github.com/microsoft/TypeScript/issues/49700
108
+ const event: AnalyticsEvent = (builder[key] as AnyFn)(...args);
42
109
 
43
- const event = {
44
- id: `editor_${eventId}`,
45
- data: {
46
- ...data,
47
- // eslint-disable-next-line camelcase
48
- job_id: context.jobId,
49
- // eslint-disable-next-line camelcase
50
- event_id: uniqueEventId,
51
- timestamp: new Date().toISOString(),
52
- },
110
+ params.sender.send({ event, metadata: METADATA });
53
111
  };
112
+ });
54
113
 
55
- return event;
56
- };
57
-
58
- return { newEvent };
59
- },
60
- /**
61
- * This function creates an event sender which will use an internal implementation of an adapter
62
- * to forward the event's data to an external third-party provider
63
- * @returns
64
- */
65
- eventSender: (adapter: AnalyticsAdapter) => {
66
- const send = (event: AnalyticsPayload) => {
67
- adapter.sendEvent({
68
- event,
69
- idParser: eventID(event.id).parse,
70
- metadata: METADATA,
71
- });
72
- };
73
-
74
- return { send };
114
+ return wrapper;
75
115
  },
76
116
  });
@@ -1,2 +1,2 @@
1
1
  export * from "./analytics";
2
- export type { CreateEventFn, AnalyticsAdapter } from "./types";
2
+ export type { AnalyticsEvent, AnalyticsEventSender } from "./types";
@@ -0,0 +1,247 @@
1
+
2
+ /* eslint-disable camelcase */
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+ // AUTO-GENERATED FILE. DO NOT EDIT.
5
+ import type { AnalyticsEvent, DefaultContext, EventFunctionArgs, GeneratedStaticFields, SchemaStaticFields } from "../types";
6
+ import { toSnakeCaseDeep } from "../utils/toSnakeCaseDeep";
7
+
8
+ /**
9
+ * Fired when the user clicks Run from the deployments page
10
+ */
11
+ export const adHocExecutionStartedDeploymentsPage = <T extends DefaultContext>(ctx: T) => (): AnalyticsEvent => {
12
+ const staticData = {
13
+ ...toSnakeCaseDeep(ctx),
14
+ unique_event_id: crypto.randomUUID(),
15
+ timestamp: new Date().toISOString(),
16
+ event_name: "adhoc_execution_started",
17
+ event_source: "cloudhome",
18
+ event_display_name: "Ad Hoc Execution Started",
19
+ interaction: {
20
+ location: "main_area",
21
+ page: "deployments",
22
+ trigger: "user",
23
+ type: "click",
24
+ },
25
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
26
+
27
+ const event = { id: staticData.event_name, data: { ...staticData } };
28
+
29
+ return event;
30
+ };
31
+
32
+ /**
33
+ * Fired when the user clicks Run in the ad hoc execution panel
34
+ */
35
+ export const adHocExecutionStartedExecutionPanel = <T extends DefaultContext>(ctx: T) => (): AnalyticsEvent => {
36
+ const staticData = {
37
+ ...toSnakeCaseDeep(ctx),
38
+ unique_event_id: crypto.randomUUID(),
39
+ timestamp: new Date().toISOString(),
40
+ event_name: "adhoc_execution_started",
41
+ event_source: "cloudhome",
42
+ event_display_name: "Ad Hoc Execution Started",
43
+ interaction: {
44
+ location: "contextual_side_panel",
45
+ page: "workflow_details",
46
+ trigger: "user",
47
+ type: "click",
48
+ },
49
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
50
+
51
+ const event = { id: staticData.event_name, data: { ...staticData } };
52
+
53
+ return event;
54
+ };
55
+
56
+ /**
57
+ * Fired when the user clicks Run from the workflow page
58
+ */
59
+ export const adHocExecutionStartedWorkflowPage = <T extends DefaultContext>(ctx: T) => (): AnalyticsEvent => {
60
+ const staticData = {
61
+ ...toSnakeCaseDeep(ctx),
62
+ unique_event_id: crypto.randomUUID(),
63
+ timestamp: new Date().toISOString(),
64
+ event_name: "adhoc_execution_started",
65
+ event_source: "cloudhome",
66
+ event_display_name: "Ad Hoc Execution Started",
67
+ interaction: {
68
+ location: "main_area",
69
+ page: "workflow_details",
70
+ trigger: "user",
71
+ type: "click",
72
+ },
73
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
74
+
75
+ const event = { id: staticData.event_name, data: { ...staticData } };
76
+
77
+ return event;
78
+ };
79
+
80
+ /**
81
+ * Fired when the user creates a new secret by clicking Create in the side panel on the team secrets page
82
+ */
83
+ export const secretCreatedTeamSecrets = <T extends DefaultContext>(ctx: T) => (): AnalyticsEvent => {
84
+ const staticData = {
85
+ ...toSnakeCaseDeep(ctx),
86
+ unique_event_id: crypto.randomUUID(),
87
+ timestamp: new Date().toISOString(),
88
+ event_name: "secret_created",
89
+ event_source: "cloudhome",
90
+ event_display_name: "Secret Created",
91
+ interaction: {
92
+ location: "contextual_side_panel",
93
+ page: "secrets",
94
+ trigger: "user",
95
+ type: "click",
96
+ },
97
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
98
+
99
+ const event = { id: staticData.event_name, data: { ...staticData } };
100
+
101
+ return event;
102
+ };
103
+
104
+ /**
105
+ * Fired when the user creates a new secret by clicking Create in the side panel on the personal secrets page
106
+ */
107
+ export const secretCreatedMySecrets = <T extends DefaultContext>(ctx: T) => (): AnalyticsEvent => {
108
+ const staticData = {
109
+ ...toSnakeCaseDeep(ctx),
110
+ unique_event_id: crypto.randomUUID(),
111
+ timestamp: new Date().toISOString(),
112
+ event_name: "secret_created",
113
+ event_source: "cloudhome",
114
+ event_display_name: "Secret Created",
115
+ interaction: {
116
+ location: "contextual_side_panel",
117
+ page: "my_secrets",
118
+ trigger: "user",
119
+ type: "click",
120
+ },
121
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
122
+
123
+ const event = { id: staticData.event_name, data: { ...staticData } };
124
+
125
+ return event;
126
+ };
127
+
128
+ /**
129
+ * Fired when the user clicks the Create button in the deployment creation side panel
130
+ */
131
+ export const deploymentCreated = <T extends DefaultContext>(ctx: T) => (eventData: EventFunctionArgs<"deploymentCreated">): AnalyticsEvent => {
132
+ const staticData = {
133
+ ...toSnakeCaseDeep(ctx),
134
+ unique_event_id: crypto.randomUUID(),
135
+ timestamp: new Date().toISOString(),
136
+ event_name: "deployment_created",
137
+ event_source: "cloudhome",
138
+ event_display_name: "Deployment Created",
139
+ interaction: {
140
+ location: "contextual_side_panel",
141
+ page: "deployments",
142
+ trigger: "user",
143
+ type: "click",
144
+ },
145
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
146
+
147
+ const event = { id: staticData.event_name, data: { ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } } };
148
+
149
+ return event;
150
+ };
151
+
152
+ /**
153
+ * Fired when the user shares a deployment via the access management side panel on the deployments page
154
+ */
155
+ export const deploymentSharedDeploymentsAccessPanel = <T extends DefaultContext>(ctx: T) => (eventData: EventFunctionArgs<"deploymentSharedDeploymentsAccessPanel">): AnalyticsEvent => {
156
+ const staticData = {
157
+ ...toSnakeCaseDeep(ctx),
158
+ unique_event_id: crypto.randomUUID(),
159
+ timestamp: new Date().toISOString(),
160
+ event_name: "deployment_shared",
161
+ event_source: "cloudhome",
162
+ event_display_name: "Deployment Shared",
163
+ interaction: {
164
+ location: "contextual_side_panel",
165
+ page: "deployments",
166
+ trigger: "user",
167
+ type: "click",
168
+ },
169
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
170
+
171
+ const event = { id: staticData.event_name, data: { ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } } };
172
+
173
+ return event;
174
+ };
175
+
176
+ /**
177
+ * Fired when the user shares a deployment via the sharing popup on the deployments page
178
+ */
179
+ export const deploymentSharedDeploymentsSharingPopup = <T extends DefaultContext>(ctx: T) => (eventData: EventFunctionArgs<"deploymentSharedDeploymentsSharingPopup">): AnalyticsEvent => {
180
+ const staticData = {
181
+ ...toSnakeCaseDeep(ctx),
182
+ unique_event_id: crypto.randomUUID(),
183
+ timestamp: new Date().toISOString(),
184
+ event_name: "deployment_shared",
185
+ event_source: "cloudhome",
186
+ event_display_name: "Deployment Shared",
187
+ interaction: {
188
+ location: "module",
189
+ page: "deployments",
190
+ trigger: "user",
191
+ type: "click",
192
+ },
193
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
194
+
195
+ const event = { id: staticData.event_name, data: { ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } } };
196
+
197
+ return event;
198
+ };
199
+
200
+ /**
201
+ * Fired when the user shares a deployment via the access management side panel on the workflow page
202
+ */
203
+ export const deploymentSharedWorkflowAccessPanel = <T extends DefaultContext>(ctx: T) => (eventData: EventFunctionArgs<"deploymentSharedWorkflowAccessPanel">): AnalyticsEvent => {
204
+ const staticData = {
205
+ ...toSnakeCaseDeep(ctx),
206
+ unique_event_id: crypto.randomUUID(),
207
+ timestamp: new Date().toISOString(),
208
+ event_name: "deployment_shared",
209
+ event_source: "cloudhome",
210
+ event_display_name: "Deployment Shared",
211
+ interaction: {
212
+ location: "contextual_side_panel",
213
+ page: "workflow_details",
214
+ trigger: "user",
215
+ type: "click",
216
+ },
217
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
218
+
219
+ const event = { id: staticData.event_name, data: { ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } } };
220
+
221
+ return event;
222
+ };
223
+
224
+ /**
225
+ * Fired when the user shares a deployment via the sharing popup on the workflow page
226
+ */
227
+ export const deploymentSharedWorkflowSharingPopup = <T extends DefaultContext>(ctx: T) => (eventData: EventFunctionArgs<"deploymentSharedWorkflowSharingPopup">): AnalyticsEvent => {
228
+ const staticData = {
229
+ ...toSnakeCaseDeep(ctx),
230
+ unique_event_id: crypto.randomUUID(),
231
+ timestamp: new Date().toISOString(),
232
+ event_name: "deployment_shared",
233
+ event_source: "cloudhome",
234
+ event_display_name: "Deployment Shared",
235
+ interaction: {
236
+ location: "module",
237
+ page: "workflow_details",
238
+ trigger: "user",
239
+ type: "click",
240
+ },
241
+ } satisfies SchemaStaticFields & GeneratedStaticFields;
242
+
243
+ const event = { id: staticData.event_name, data: { ...staticData, payload: { ...toSnakeCaseDeep(eventData as Record<string, any>) } } };
244
+
245
+ return event;
246
+ };
247
+