@node-red/runtime 4.0.8 → 4.1.0-beta.1
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/lib/api/context.js +26 -6
- package/lib/api/settings.js +14 -0
- package/lib/flows/Flow.js +11 -0
- package/lib/flows/Group.js +8 -0
- package/lib/flows/util.js +18 -1
- package/lib/index.js +8 -1
- package/lib/storage/localfilesystem/projects/git/index.js +2 -0
- package/lib/telemetry/index.js +213 -0
- package/lib/telemetry/metrics/01-core.js +5 -0
- package/lib/telemetry/metrics/02-os.js +9 -0
- package/lib/telemetry/metrics/03-env.js +8 -0
- package/package.json +7 -5
package/lib/api/context.js
CHANGED
|
@@ -96,7 +96,11 @@ var api = module.exports = {
|
|
|
96
96
|
} else if (scope === 'node') {
|
|
97
97
|
var node = runtime.nodes.getNode(id);
|
|
98
98
|
if (node) {
|
|
99
|
-
|
|
99
|
+
if (/^subflow:/.test(node.type)) {
|
|
100
|
+
ctx = runtime.nodes.getContext(node.id);
|
|
101
|
+
} else {
|
|
102
|
+
ctx = node.context();
|
|
103
|
+
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
if (ctx) {
|
|
@@ -104,13 +108,25 @@ var api = module.exports = {
|
|
|
104
108
|
store = store || availableStores.default;
|
|
105
109
|
ctx.get(key,store,function(err, v) {
|
|
106
110
|
if (opts.keysOnly) {
|
|
111
|
+
const result = {}
|
|
107
112
|
if (Array.isArray(v)) {
|
|
108
|
-
|
|
113
|
+
result.format = `array[${v.length}]`
|
|
109
114
|
} else if (typeof v === 'object') {
|
|
110
|
-
|
|
115
|
+
result.keys = Object.keys(v).map(k => {
|
|
116
|
+
if (Array.isArray(v[k])) {
|
|
117
|
+
return { key: k, format: `array[${v[k].length}]`, length: v[k].length }
|
|
118
|
+
} else if (typeof v[k] === 'object') {
|
|
119
|
+
return { key: k, format: 'object' }
|
|
120
|
+
} else {
|
|
121
|
+
return { key: k }
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
result.format = 'object'
|
|
111
125
|
} else {
|
|
112
|
-
|
|
126
|
+
result.keys = []
|
|
113
127
|
}
|
|
128
|
+
resolve({ [store]: result })
|
|
129
|
+
return
|
|
114
130
|
}
|
|
115
131
|
var encoded = util.encodeObject({msg:v});
|
|
116
132
|
if (store !== availableStores.default) {
|
|
@@ -147,7 +163,7 @@ var api = module.exports = {
|
|
|
147
163
|
}
|
|
148
164
|
return
|
|
149
165
|
}
|
|
150
|
-
result[store] = { keys }
|
|
166
|
+
result[store] = { keys: keys.map(key => { return { key }}) }
|
|
151
167
|
c--;
|
|
152
168
|
if (c === 0) {
|
|
153
169
|
if (!errorReported) {
|
|
@@ -225,7 +241,11 @@ var api = module.exports = {
|
|
|
225
241
|
} else if (scope === 'node') {
|
|
226
242
|
var node = runtime.nodes.getNode(id);
|
|
227
243
|
if (node) {
|
|
228
|
-
|
|
244
|
+
if (/^subflow:/.test(node.type)) {
|
|
245
|
+
ctx = runtime.nodes.getContext(node.id);
|
|
246
|
+
} else {
|
|
247
|
+
ctx = node.context();
|
|
248
|
+
}
|
|
229
249
|
}
|
|
230
250
|
}
|
|
231
251
|
if (ctx) {
|
package/lib/api/settings.js
CHANGED
|
@@ -161,6 +161,8 @@ var api = module.exports = {
|
|
|
161
161
|
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
|
|
165
|
+
|
|
164
166
|
safeSettings.runtimeState = {
|
|
165
167
|
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
|
|
166
168
|
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
|
|
@@ -213,7 +215,19 @@ var api = module.exports = {
|
|
|
213
215
|
}
|
|
214
216
|
var currentSettings = runtime.settings.getUserSettings(username)||{};
|
|
215
217
|
currentSettings = extend(currentSettings, opts.settings);
|
|
218
|
+
|
|
216
219
|
try {
|
|
220
|
+
if (currentSettings.hasOwnProperty("telemetryEnabled")) {
|
|
221
|
+
// This is a global setting that is being set by the user. It should
|
|
222
|
+
// not be stored per-user as it applies to the whole runtime.
|
|
223
|
+
const telemetryEnabled = currentSettings.telemetryEnabled;
|
|
224
|
+
delete currentSettings.telemetryEnabled;
|
|
225
|
+
if (telemetryEnabled) {
|
|
226
|
+
runtime.telemetry.enable()
|
|
227
|
+
} else {
|
|
228
|
+
runtime.telemetry.disable()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
217
231
|
return runtime.settings.setUserSettings(username, currentSettings).then(function() {
|
|
218
232
|
runtime.log.audit({event: "settings.update",username:username}, opts.req);
|
|
219
233
|
return;
|
package/lib/flows/Flow.js
CHANGED
|
@@ -675,6 +675,9 @@ class Flow {
|
|
|
675
675
|
count: count
|
|
676
676
|
}
|
|
677
677
|
};
|
|
678
|
+
if (logMessage.hasOwnProperty('code')) {
|
|
679
|
+
errorMessage.error.code = logMessage.code;
|
|
680
|
+
}
|
|
678
681
|
if (logMessage.hasOwnProperty('stack')) {
|
|
679
682
|
errorMessage.error.stack = logMessage.stack;
|
|
680
683
|
}
|
|
@@ -719,6 +722,14 @@ class Flow {
|
|
|
719
722
|
});
|
|
720
723
|
}
|
|
721
724
|
|
|
725
|
+
getContext(scope) {
|
|
726
|
+
if (scope === 'flow') {
|
|
727
|
+
return this.context
|
|
728
|
+
} else if (scope === 'global') {
|
|
729
|
+
return context.get('global')
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
722
733
|
dump() {
|
|
723
734
|
console.log("==================")
|
|
724
735
|
console.log(this.TYPE, this.id);
|
package/lib/flows/Group.js
CHANGED
package/lib/flows/util.js
CHANGED
|
@@ -100,7 +100,24 @@ async function evaluateEnvProperties(flow, env, credentials) {
|
|
|
100
100
|
}
|
|
101
101
|
} else if (type ==='jsonata') {
|
|
102
102
|
pendingEvaluations.push(new Promise((resolve, _) => {
|
|
103
|
-
redUtil.evaluateNodeProperty(value, 'jsonata',
|
|
103
|
+
redUtil.evaluateNodeProperty(value, 'jsonata',{
|
|
104
|
+
// Fake a node object to provide access to _flow and context
|
|
105
|
+
_flow: flow,
|
|
106
|
+
context: () => {
|
|
107
|
+
return {
|
|
108
|
+
flow: {
|
|
109
|
+
get: (value, store, callback) => {
|
|
110
|
+
return flow.getContext('flow').get(value, store, callback)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
global: {
|
|
114
|
+
get: (value, store, callback) => {
|
|
115
|
+
return flow.getContext('global').get(value, store, callback)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, null, (err, result) => {
|
|
104
121
|
if (!err) {
|
|
105
122
|
if (typeof result === 'object') {
|
|
106
123
|
result = { value: result, __clone__: true}
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ var library = require("./library");
|
|
|
23
23
|
var plugins = require("./plugins");
|
|
24
24
|
var settings = require("./settings");
|
|
25
25
|
const multiplayer = require("./multiplayer");
|
|
26
|
+
const telemetry = require("./telemetry");
|
|
26
27
|
|
|
27
28
|
var express = require("express");
|
|
28
29
|
var path = require('path');
|
|
@@ -135,6 +136,7 @@ function start() {
|
|
|
135
136
|
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
|
|
136
137
|
.then(function() { return storage.init(runtime)})
|
|
137
138
|
.then(function() { return settings.load(storage)})
|
|
139
|
+
.then(function() { return telemetry.init(runtime)})
|
|
138
140
|
.then(function() { return library.init(runtime)})
|
|
139
141
|
.then(function() { return multiplayer.init(runtime)})
|
|
140
142
|
.then(function() {
|
|
@@ -235,8 +237,12 @@ function start() {
|
|
|
235
237
|
}
|
|
236
238
|
}
|
|
237
239
|
return redNodes.loadContextsPlugin().then(function () {
|
|
238
|
-
redNodes.loadFlows().then(() => { redNodes.startFlows() }).catch(function(err) {});
|
|
239
240
|
started = true;
|
|
241
|
+
redNodes.loadFlows().then(() => {
|
|
242
|
+
if (started) {
|
|
243
|
+
redNodes.startFlows()
|
|
244
|
+
}
|
|
245
|
+
}).catch(function(err) {});
|
|
240
246
|
});
|
|
241
247
|
});
|
|
242
248
|
});
|
|
@@ -337,6 +343,7 @@ var runtime = {
|
|
|
337
343
|
library: library,
|
|
338
344
|
exec: exec,
|
|
339
345
|
util: util,
|
|
346
|
+
telemetry: telemetry,
|
|
340
347
|
get adminApi() { return adminApi },
|
|
341
348
|
get adminApp() { return adminApp },
|
|
342
349
|
get nodeApp() { return nodeApp },
|
|
@@ -51,6 +51,8 @@ function runGitCommand(args,cwd,env,emit) {
|
|
|
51
51
|
err.code = "git_auth_failed";
|
|
52
52
|
} else if(/Authentication failed/i.test(stderr)) {
|
|
53
53
|
err.code = "git_auth_failed";
|
|
54
|
+
} else if (/The requested URL returned error: 403/i.test(stderr)) {
|
|
55
|
+
err.code = "git_auth_failed";
|
|
54
56
|
} else if (/commit your changes or stash/i.test(stderr)) {
|
|
55
57
|
err.code = "git_local_overwrite";
|
|
56
58
|
} else if (/CONFLICT/.test(err.stdout)) {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs/promises')
|
|
3
|
+
const semver = require('semver')
|
|
4
|
+
const cronosjs = require('cronosjs')
|
|
5
|
+
|
|
6
|
+
const METRICS_DIR = path.join(__dirname, 'metrics')
|
|
7
|
+
const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
|
|
8
|
+
|
|
9
|
+
/** @type {import("got").Got | undefined} */
|
|
10
|
+
let got
|
|
11
|
+
|
|
12
|
+
let runtime
|
|
13
|
+
|
|
14
|
+
let scheduleTask
|
|
15
|
+
|
|
16
|
+
async function gather () {
|
|
17
|
+
let metricFiles = await fs.readdir(METRICS_DIR)
|
|
18
|
+
metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
|
|
19
|
+
metricFiles.sort()
|
|
20
|
+
|
|
21
|
+
const metrics = {}
|
|
22
|
+
|
|
23
|
+
for (let i = 0, l = metricFiles.length; i < l; i++) {
|
|
24
|
+
const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
|
|
25
|
+
let result = metricModule(runtime)
|
|
26
|
+
if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
|
|
27
|
+
result = await result
|
|
28
|
+
}
|
|
29
|
+
const keys = Object.keys(result)
|
|
30
|
+
keys.forEach(key => {
|
|
31
|
+
const keyParts = key.split('.')
|
|
32
|
+
let p = metrics
|
|
33
|
+
keyParts.forEach((part, index) => {
|
|
34
|
+
if (index < keyParts.length - 1) {
|
|
35
|
+
if (!p[part]) {
|
|
36
|
+
p[part] = {}
|
|
37
|
+
}
|
|
38
|
+
p = p[part]
|
|
39
|
+
} else {
|
|
40
|
+
p[part] = result[key]
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return metrics
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function report () {
|
|
49
|
+
if (!isTelemetryEnabled()) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
// If enabled, gather metrics
|
|
53
|
+
const metrics = await gather()
|
|
54
|
+
|
|
55
|
+
// Post metrics to endpoint - handle any error silently
|
|
56
|
+
|
|
57
|
+
if (!got) {
|
|
58
|
+
got = (await import('got')).got
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
runtime.log.debug('Sending telemetry')
|
|
62
|
+
const response = await got.post('https://telemetry.nodered.org/ping', {
|
|
63
|
+
json: metrics,
|
|
64
|
+
responseType: 'json',
|
|
65
|
+
headers: {
|
|
66
|
+
'User-Agent': `Node-RED/${runtime.settings.version}`
|
|
67
|
+
}
|
|
68
|
+
}).json().catch(err => {
|
|
69
|
+
// swallow errors
|
|
70
|
+
runtime.log.debug('Failed to send telemetry: ' + err.toString())
|
|
71
|
+
})
|
|
72
|
+
// Example response:
|
|
73
|
+
// { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
|
|
74
|
+
runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
|
|
75
|
+
// Get response from endpoint
|
|
76
|
+
if (response?.['node-red']) {
|
|
77
|
+
const currentVersion = metrics.env['node-red']
|
|
78
|
+
if (semver.valid(currentVersion)) {
|
|
79
|
+
const latest = response['node-red'].latest
|
|
80
|
+
const next = response['node-red'].next
|
|
81
|
+
let updatePayload
|
|
82
|
+
if (semver.lt(currentVersion, latest)) {
|
|
83
|
+
// Case one: current < latest
|
|
84
|
+
runtime.log.info(`A new version of Node-RED is available: ${latest}`)
|
|
85
|
+
updatePayload = { version: latest }
|
|
86
|
+
} else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
|
|
87
|
+
// Case two: current > latest && current < next
|
|
88
|
+
runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
|
|
89
|
+
updatePayload = { version: next }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updatePayload && isUpdateNotificationEnabled()) {
|
|
93
|
+
runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isTelemetryEnabled () {
|
|
100
|
+
// If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
|
|
101
|
+
// the settings object will have been updated to disable telemetry explicitly
|
|
102
|
+
|
|
103
|
+
// If there are no telemetry settings then the user has not had a chance
|
|
104
|
+
// to opt out yet - so keep it disabled until they do
|
|
105
|
+
|
|
106
|
+
let telemetrySettings
|
|
107
|
+
try {
|
|
108
|
+
telemetrySettings = runtime.settings.get('telemetry')
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// Settings not available
|
|
111
|
+
}
|
|
112
|
+
let runtimeTelemetryEnabled
|
|
113
|
+
try {
|
|
114
|
+
runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Settings not available
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
|
|
120
|
+
// No telemetry settings - so keep it disabled
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// User has made a choice; defer to that
|
|
125
|
+
if (runtimeTelemetryEnabled !== undefined) {
|
|
126
|
+
return runtimeTelemetryEnabled
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If there are telemetry settings, use what it says
|
|
130
|
+
if (telemetrySettings && telemetrySettings.enabled !== undefined) {
|
|
131
|
+
return telemetrySettings.enabled
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// At this point, we have no sign the user has consented to telemetry, so
|
|
135
|
+
// keep disabled - but return undefined as a false-like value to distinguish
|
|
136
|
+
// it from the explicit disable above
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isUpdateNotificationEnabled () {
|
|
141
|
+
const telemetrySettings = runtime.settings.get('telemetry') || {}
|
|
142
|
+
return telemetrySettings.updateNotification !== false
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Start the telemetry schedule
|
|
146
|
+
*/
|
|
147
|
+
function startTelemetry () {
|
|
148
|
+
if (scheduleTask) {
|
|
149
|
+
// Already scheduled - nothing left to do
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
|
|
154
|
+
const pingMinutes = pingTime.getMinutes()
|
|
155
|
+
const pingHours = pingTime.getHours()
|
|
156
|
+
const pingSchedule = `${pingMinutes} ${pingHours} * * *`
|
|
157
|
+
|
|
158
|
+
runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
|
|
159
|
+
|
|
160
|
+
scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
|
|
161
|
+
report()
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stopTelemetry () {
|
|
166
|
+
if (scheduleTask) {
|
|
167
|
+
runtime.log.debug(`Telemetry disabled`)
|
|
168
|
+
scheduleTask.stop()
|
|
169
|
+
scheduleTask = null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
init: (_runtime) => {
|
|
175
|
+
runtime = _runtime
|
|
176
|
+
|
|
177
|
+
if (isTelemetryEnabled()) {
|
|
178
|
+
startTelemetry()
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* Enable telemetry via user opt-in in the editor
|
|
183
|
+
*/
|
|
184
|
+
enable: () => {
|
|
185
|
+
if (runtime.settings.available()) {
|
|
186
|
+
runtime.settings.set('telemetryEnabled', true)
|
|
187
|
+
}
|
|
188
|
+
startTelemetry()
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Disable telemetry via user opt-in in the editor
|
|
193
|
+
*/
|
|
194
|
+
disable: () => {
|
|
195
|
+
if (runtime.settings.available()) {
|
|
196
|
+
runtime.settings.set('telemetryEnabled', false)
|
|
197
|
+
}
|
|
198
|
+
stopTelemetry()
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get telemetry enabled status
|
|
203
|
+
* @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
|
|
204
|
+
*/
|
|
205
|
+
isEnabled: isTelemetryEnabled,
|
|
206
|
+
|
|
207
|
+
stop: () => {
|
|
208
|
+
if (scheduleTask) {
|
|
209
|
+
scheduleTask.stop()
|
|
210
|
+
scheduleTask = null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node-red/runtime",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.1.0-beta.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -16,13 +16,15 @@
|
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@node-red/registry": "4.0.
|
|
20
|
-
"@node-red/util": "4.0.
|
|
19
|
+
"@node-red/registry": "4.1.0-beta.1",
|
|
20
|
+
"@node-red/util": "4.1.0-beta.1",
|
|
21
21
|
"async-mutex": "0.5.0",
|
|
22
22
|
"clone": "2.1.2",
|
|
23
|
+
"cronosjs": "1.7.1",
|
|
23
24
|
"express": "4.21.2",
|
|
24
|
-
"fs-extra": "11.
|
|
25
|
+
"fs-extra": "11.3.0",
|
|
25
26
|
"json-stringify-safe": "5.0.1",
|
|
26
|
-
"rfdc": "^1.3.1"
|
|
27
|
+
"rfdc": "^1.3.1",
|
|
28
|
+
"semver": "7.7.1"
|
|
27
29
|
}
|
|
28
30
|
}
|