@miso.ai/client-sdk-core 1.13.0-beta.0 → 1.13.0-beta.2

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.
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _commons = require("@miso.ai/commons");
8
+ function _objectDestructuringEmpty(t) { if (null == t) throw new TypeError("Cannot destructure " + t); }
9
+ const PLUGIN_ID = 'std:analytics';
10
+ class AnalyticsPlugin extends _commons.Component {
11
+ static get id() {
12
+ return PLUGIN_ID;
13
+ }
14
+ constructor() {
15
+ super('analytics');
16
+ this._members = [];
17
+ this._engagement = undefined;
18
+ }
19
+ config(options = {}) {
20
+ this._options = options;
21
+ for (const member of this._members) {
22
+ // notify update
23
+ // TODO: merge with global options
24
+ //member.config(options);
25
+ }
26
+ }
27
+ install(MisoClient, context) {
28
+ context.addSubtree(this);
29
+ this._engagement = new _commons.UserEngagementObserver(this._handleEngagement.bind(this));
30
+ // TODO: should we provide a way to stop the observer?
31
+ MisoClient.on('create', this._injectClient.bind(this));
32
+ }
33
+ _injectClient(client) {
34
+ client.on('workflow', this._injectWorkflow.bind(this));
35
+ }
36
+ _injectWorkflow(workflow) {
37
+ workflow.analytics = new Analytics(this, workflow);
38
+ }
39
+ _handleEngagement() {
40
+ for (const member of this._members) {
41
+ member._syncEngagementState();
42
+ }
43
+ }
44
+ }
45
+ exports.default = AnalyticsPlugin;
46
+ function normalizeOptions(_ref = {}) {
47
+ let options = Object.assign({}, (_objectDestructuringEmpty(_ref), _ref));
48
+ return Object.assign({}, options);
49
+ }
50
+ function mergeTwoOptions(base, overrides) {
51
+ return Object.assign({}, base, overrides);
52
+ }
53
+ class Analytics {
54
+ constructor(plugin, workflow) {
55
+ this._plugin = plugin;
56
+ plugin._members.push(this);
57
+ this._workflow = workflow;
58
+ this._timer = new _commons.Stopwatch();
59
+ this._options = normalizeOptions();
60
+ this._injectWorkflow(workflow);
61
+ this._session = undefined;
62
+ this._startAt = undefined;
63
+ this._loadingAt = undefined;
64
+ this._readyAt = undefined;
65
+ this.config();
66
+ }
67
+ _injectWorkflow(workflow) {
68
+ this._listenToLiftcycleEvents(workflow);
69
+ this._injectProcessInteraction(workflow);
70
+ }
71
+ _listenToLiftcycleEvents(workflow) {
72
+ workflow.on('session', this._handleSession.bind(this));
73
+ workflow.on('loading', this._handleLoading.bind(this));
74
+ workflow.on('ready', this._handleReady.bind(this));
75
+ }
76
+ _injectProcessInteraction(workflow) {
77
+ // TODO: ad-hoc, find a way to provide plugin API in plugin root
78
+ workflow._pluginContext.processInteractionPasses.push(this._writeStateToInteraction.bind(this));
79
+ }
80
+ _handleSession(session) {
81
+ if (this._session && session.index === this._session.index) {
82
+ return;
83
+ }
84
+ if (this._session) {
85
+ // send a last heartbeat event for the old session
86
+ this._sendHeartbeat();
87
+ }
88
+ this._session = session;
89
+ this._timer.reset();
90
+ this._startAt = Date.now();
91
+ this._loadingAt = undefined;
92
+ this._readyAt = undefined;
93
+ }
94
+ _handleLoading({
95
+ session
96
+ } = {}) {
97
+ this._loadingAt = Date.now();
98
+ }
99
+ _handleReady({
100
+ session
101
+ } = {}) {
102
+ this._readyAt = Date.now();
103
+ // start timer if user is engaged
104
+ this._syncEngagementState();
105
+ }
106
+ _sendHeartbeat() {
107
+ const tracker = this._workflow.trackers.container;
108
+ tracker && tracker.heartbeat();
109
+ }
110
+ _writeStateToInteraction(payload) {
111
+ const {
112
+ state
113
+ } = this;
114
+ return (0, _commons.mergeInteractions)(payload, {
115
+ context: {
116
+ custom_context: {
117
+ analytics_timestamp: state.timestamp,
118
+ user_engagement_time: state.userEngagementTime,
119
+ user_idle_time: state.userIdleTime,
120
+ user_waiting_time: state.userWaitingTime
121
+ }
122
+ }
123
+ });
124
+ }
125
+ config(options = {}) {
126
+ this._options = mergeTwoOptions(this._options, normalizeOptions(options));
127
+ }
128
+ get options() {
129
+ return Object.assign({}, this._options);
130
+ }
131
+ get state() {
132
+ const currentTime = Date.now();
133
+ const loadingAt = this._loadingAt;
134
+ const readyAt = this._readyAt;
135
+ let userEngagementTime = 0,
136
+ userIdleTime = 0,
137
+ userWaitingTime = 0;
138
+ if (loadingAt !== undefined) {
139
+ userWaitingTime = currentTime - loadingAt;
140
+ }
141
+ if (readyAt !== undefined) {
142
+ userWaitingTime = readyAt - loadingAt;
143
+ userEngagementTime = this._timer.getDuration(currentTime);
144
+ userIdleTime = currentTime - readyAt - userEngagementTime;
145
+ }
146
+ return (0, _commons.trimObj)({
147
+ timestamp: currentTime,
148
+ userWaitingTime,
149
+ userEngagementTime,
150
+ userIdleTime
151
+ });
152
+ }
153
+ _syncEngagementState() {
154
+ const shallStart = this._plugin._engagement.engaged;
155
+ if (shallStart) {
156
+ this._timer.start();
157
+ } else {
158
+ this._timer.stop();
159
+ }
160
+ }
161
+ }
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "AnalyticsPlugin", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _analytics.default;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "ApiPatchPlugin", {
7
13
  enumerable: true,
8
14
  get: function () {
@@ -52,4 +58,5 @@ var _interactions = _interopRequireDefault(require("./interactions.js"));
52
58
  var _nativeFetch = _interopRequireDefault(require("./native-fetch.js"));
53
59
  var _apiPatch = _interopRequireDefault(require("./api-patch.js"));
54
60
  var _headerApiKey = _interopRequireDefault(require("./header-api-key.js"));
61
+ var _analytics = _interopRequireDefault(require("./analytics.js"));
55
62
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -4,4 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _default = exports.default = '1.13.0-beta.0';
7
+ var _default = exports.default = '1.13.0-beta.2';
package/package.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
30
30
  },
31
31
  "dependencies": {
32
- "@miso.ai/commons": "1.13.0-beta.0"
32
+ "@miso.ai/commons": "1.13.0-beta.2"
33
33
  },
34
- "version": "1.13.0-beta.0"
34
+ "version": "1.13.0-beta.2"
35
35
  }
@@ -0,0 +1,106 @@
1
+ # Plugin System
2
+
3
+ This directory contains the core plugin infrastructure and standard plugins for the Miso Client SDK.
4
+
5
+ ## Architecture
6
+
7
+ ### PluginRoot (`plugin-root.js`)
8
+
9
+ The central registry that manages plugin lifecycle:
10
+
11
+ - **Registration**: `register(PluginClass)` - Makes a plugin available by its ID
12
+ - **Activation**: `use(id, options)` - Instantiates and installs a registered plugin
13
+ - **Installation**: `install(id, options)` - Combines register + use, supports remote loading for `std:*` plugins
14
+
15
+ ### PluginContext
16
+
17
+ Exposed to plugins during `install()`, provides hooks into the SDK:
18
+
19
+ ```javascript
20
+ context.addPayloadPass(fn) // Modify API request payloads
21
+ context.addHeadersPass(fn) // Modify request headers
22
+ context.addUrlPass(fn) // Modify request URLs
23
+ context.setCustomFetch(fn) // Override fetch implementation
24
+ context.setCustomSendBeacon(fn) // Override sendBeacon
25
+ context.addSubtree(component) // Register as component subtree for event propagation
26
+ context.onHubUpdate(fn) // Listen to hub state updates
27
+ context.onHubEmit(fn) // Listen to hub emissions
28
+ context.whenInstalled(id) // Wait for another plugin
29
+ context.classes // Access to SDK classes
30
+ ```
31
+
32
+ ## Creating a Plugin
33
+
34
+ ### Basic Structure
35
+
36
+ ```javascript
37
+ const PLUGIN_ID = 'std:my-plugin';
38
+
39
+ export default class MyPlugin {
40
+ static get id() {
41
+ return PLUGIN_ID;
42
+ }
43
+
44
+ // Optional: extend Component for event support
45
+ // constructor() { super('my-plugin'); }
46
+
47
+ install(MisoClient, context) {
48
+ // Setup logic here
49
+ MisoClient.on('create', this._injectClient.bind(this));
50
+ }
51
+
52
+ // Optional: allow runtime configuration
53
+ config(options) {
54
+ this._options = options;
55
+ }
56
+
57
+ _injectClient(client) {
58
+ // Extend client instances
59
+ client.myFeature = new MyFeature(this, client);
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Plugin Patterns
65
+
66
+ 1. **Payload Modifier** (e.g., `context.js`, `interactions.js`): Intercept and modify API payloads
67
+ 2. **Client Injector**: Add properties/methods to each MisoClient instance
68
+ 3. **Component-based** (extend `Component`): For plugins needing event emission and subtree registration
69
+
70
+ ## Standard Plugins
71
+
72
+ | Plugin | ID | Purpose |
73
+ |--------|-----|---------|
74
+ | `ContextPlugin` | `std:context` | User identity (anonymous_id, user_id, site) |
75
+ | `PageInfoPlugin` | `std:page-info` | Adds page metadata to requests |
76
+ | `AutoEventsPlugin` | `std:auto-events` | Automatic interaction tracking |
77
+ | `InteractionsPlugin` | `std:interactions` | Enriches interaction records with SDK version/UUID |
78
+ | `NativeFetchPlugin` | `std:native-fetch` | Uses native fetch, bypassing custom implementations |
79
+ | `ApiPatchPlugin` | `std:api-patch` | Applies patches to API responses |
80
+ | `HeaderApiKeyPlugin` | `std:header-api-key` | Sends API key in header instead of query |
81
+ | `AnalyticsPlugin` | `std:analytics` | User engagement timing and analytics |
82
+
83
+ ## Pass Functions
84
+
85
+ Pass functions receive a context object and return modified values:
86
+
87
+ ```javascript
88
+ // Payload pass signature
89
+ function modifyPayload({ client, apiGroup, apiName, payload }) {
90
+ // apiGroup: 'search', 'ask', 'interactions', etc.
91
+ // apiName: 'search', 'upload', 'questions', etc.
92
+ return modifiedPayload;
93
+ }
94
+
95
+ // Headers pass signature
96
+ function modifyHeaders({ client, headers }) {
97
+ return { ...headers, 'X-Custom': 'value' };
98
+ }
99
+ ```
100
+
101
+ ## Lifecycle
102
+
103
+ 1. `register()` - Plugin class stored by ID
104
+ 2. `use()` - Plugin instantiated, `config()` called if options provided
105
+ 3. `install()` - Plugin's `install(MisoClient, context)` called
106
+ 4. `MisoClient.on('create')` - Hook to extend new client instances
@@ -0,0 +1,185 @@
1
+ import { Component, UserEngagementObserver, Stopwatch, trimObj, mergeInteractions } from '@miso.ai/commons';
2
+
3
+ const PLUGIN_ID = 'std:analytics';
4
+
5
+ export default class AnalyticsPlugin extends Component {
6
+
7
+ static get id() {
8
+ return PLUGIN_ID;
9
+ }
10
+
11
+ constructor() {
12
+ super('analytics');
13
+ this._members = [];
14
+ this._engagement = undefined;
15
+ }
16
+
17
+ config(options = {}) {
18
+ this._options = options;
19
+ for (const member of this._members) {
20
+ // notify update
21
+ // TODO: merge with global options
22
+ //member.config(options);
23
+ }
24
+ }
25
+
26
+ install(MisoClient, context) {
27
+ context.addSubtree(this);
28
+ this._engagement = new UserEngagementObserver(this._handleEngagement.bind(this));
29
+ // TODO: should we provide a way to stop the observer?
30
+ MisoClient.on('create', this._injectClient.bind(this));
31
+ }
32
+
33
+ _injectClient(client) {
34
+ client.on('workflow', this._injectWorkflow.bind(this));
35
+ }
36
+
37
+ _injectWorkflow(workflow) {
38
+ workflow.analytics = new Analytics(this, workflow);
39
+ }
40
+
41
+ _handleEngagement() {
42
+ for (const member of this._members) {
43
+ member._syncEngagementState();
44
+ }
45
+ }
46
+
47
+ }
48
+
49
+ function normalizeOptions({ ...options } = {}) {
50
+ return {
51
+ ...options,
52
+ };
53
+ }
54
+
55
+ function mergeTwoOptions(base, overrides) {
56
+ return {
57
+ ...base,
58
+ ...overrides,
59
+ };
60
+ }
61
+
62
+ class Analytics {
63
+
64
+ constructor(plugin, workflow) {
65
+ this._plugin = plugin;
66
+ plugin._members.push(this);
67
+ this._workflow = workflow;
68
+ this._timer = new Stopwatch();
69
+ this._options = normalizeOptions();
70
+
71
+ this._injectWorkflow(workflow);
72
+
73
+ this._session = undefined;
74
+ this._startAt = undefined;
75
+ this._loadingAt = undefined;
76
+ this._readyAt = undefined;
77
+
78
+ this.config();
79
+ }
80
+
81
+ _injectWorkflow(workflow) {
82
+ this._listenToLiftcycleEvents(workflow);
83
+ this._injectProcessInteraction(workflow);
84
+ }
85
+
86
+ _listenToLiftcycleEvents(workflow) {
87
+ workflow.on('session', this._handleSession.bind(this));
88
+ workflow.on('loading', this._handleLoading.bind(this));
89
+ workflow.on('ready', this._handleReady.bind(this));
90
+ }
91
+
92
+ _injectProcessInteraction(workflow) {
93
+ // TODO: ad-hoc, find a way to provide plugin API in plugin root
94
+ workflow._pluginContext.processInteractionPasses.push(this._writeStateToInteraction.bind(this));
95
+ }
96
+
97
+ _handleSession(session) {
98
+ if (this._session && session.index === this._session.index) {
99
+ return;
100
+ }
101
+
102
+ if (this._session) {
103
+ // send a last heartbeat event for the old session
104
+ this._sendHeartbeat();
105
+ }
106
+
107
+ this._session = session;
108
+ this._timer.reset();
109
+ this._startAt = Date.now();
110
+ this._loadingAt = undefined;
111
+ this._readyAt = undefined;
112
+ }
113
+
114
+ _handleLoading({ session } = {}) {
115
+ this._loadingAt = Date.now();
116
+ }
117
+
118
+ _handleReady({ session } = {}) {
119
+ this._readyAt = Date.now();
120
+ // start timer if user is engaged
121
+ this._syncEngagementState();
122
+ }
123
+
124
+ _sendHeartbeat() {
125
+ const tracker = this._workflow.trackers.container;
126
+ tracker && tracker.heartbeat();
127
+ }
128
+
129
+ _writeStateToInteraction(payload) {
130
+ const { state } = this;
131
+ return mergeInteractions(payload, {
132
+ context: {
133
+ custom_context: {
134
+ analytics_timestamp: state.timestamp,
135
+ user_engagement_time: state.userEngagementTime,
136
+ user_idle_time: state.userIdleTime,
137
+ user_waiting_time: state.userWaitingTime,
138
+ },
139
+ },
140
+ });
141
+ }
142
+
143
+ config(options = {}) {
144
+ this._options = mergeTwoOptions(this._options, normalizeOptions(options));
145
+ }
146
+
147
+ get options() {
148
+ return {
149
+ ...this._options,
150
+ };
151
+ }
152
+
153
+ get state() {
154
+ const currentTime = Date.now();
155
+ const loadingAt = this._loadingAt;
156
+ const readyAt = this._readyAt;
157
+ let userEngagementTime = 0, userIdleTime = 0, userWaitingTime = 0;
158
+
159
+ if (loadingAt !== undefined) {
160
+ userWaitingTime = currentTime - loadingAt;
161
+ }
162
+ if (readyAt !== undefined) {
163
+ userWaitingTime = readyAt - loadingAt;
164
+ userEngagementTime = this._timer.getDuration(currentTime);
165
+ userIdleTime = currentTime - readyAt - userEngagementTime;
166
+ }
167
+
168
+ return trimObj({
169
+ timestamp: currentTime,
170
+ userWaitingTime,
171
+ userEngagementTime,
172
+ userIdleTime,
173
+ });
174
+ }
175
+
176
+ _syncEngagementState() {
177
+ const shallStart = this._plugin._engagement.engaged;
178
+ if (shallStart) {
179
+ this._timer.start();
180
+ } else {
181
+ this._timer.stop();
182
+ }
183
+ }
184
+
185
+ }
@@ -5,3 +5,4 @@ export { default as InteractionsPlugin } from './interactions.js';
5
5
  export { default as NativeFetchPlugin } from './native-fetch.js';
6
6
  export { default as ApiPatchPlugin } from './api-patch.js';
7
7
  export { default as HeaderApiKeyPlugin } from './header-api-key.js';
8
+ export { default as AnalyticsPlugin } from './analytics.js';
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.13.0-beta.0';
1
+ export default '1.13.0-beta.2';