mirror of
https://github.com/node-red/node-red.git
synced 2025-12-27 07:31:07 +01:00
Add runtime telemetry component
This commit is contained in:
@@ -161,6 +161,8 @@ var api = module.exports = {
|
||||
safeSettings.diagnostics.ui = false; // cannot have UI without endpoint
|
||||
}
|
||||
|
||||
safeSettings.telemetryEnabled = runtime.telemetry.isEnabled()
|
||||
|
||||
safeSettings.runtimeState = {
|
||||
//unless runtimeState.ui and runtimeState.enabled are explicitly true, they will default to false.
|
||||
enabled: !!runtime.settings.runtimeState && runtime.settings.runtimeState.enabled === true,
|
||||
@@ -213,7 +215,19 @@ var api = module.exports = {
|
||||
}
|
||||
var currentSettings = runtime.settings.getUserSettings(username)||{};
|
||||
currentSettings = extend(currentSettings, opts.settings);
|
||||
|
||||
try {
|
||||
if (currentSettings.hasOwnProperty("telemetryEnabled")) {
|
||||
// This is a global setting that is being set by the user. It should
|
||||
// not be stored per-user as it applies to the whole runtime.
|
||||
const telemetryEnabled = currentSettings.telemetryEnabled;
|
||||
delete currentSettings.telemetryEnabled;
|
||||
if (telemetryEnabled) {
|
||||
runtime.telemetry.enable()
|
||||
} else {
|
||||
runtime.telemetry.disable()
|
||||
}
|
||||
}
|
||||
return runtime.settings.setUserSettings(username, currentSettings).then(function() {
|
||||
runtime.log.audit({event: "settings.update",username:username}, opts.req);
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@ var library = require("./library");
|
||||
var plugins = require("./plugins");
|
||||
var settings = require("./settings");
|
||||
const multiplayer = require("./multiplayer");
|
||||
const telemetry = require("./telemetry");
|
||||
|
||||
var express = require("express");
|
||||
var path = require('path');
|
||||
@@ -135,6 +136,7 @@ function start() {
|
||||
return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"..","locales")),"runtime.json")
|
||||
.then(function() { return storage.init(runtime)})
|
||||
.then(function() { return settings.load(storage)})
|
||||
.then(function() { return telemetry.init(runtime)})
|
||||
.then(function() { return library.init(runtime)})
|
||||
.then(function() { return multiplayer.init(runtime)})
|
||||
.then(function() {
|
||||
@@ -337,6 +339,7 @@ var runtime = {
|
||||
library: library,
|
||||
exec: exec,
|
||||
util: util,
|
||||
telemetry: telemetry,
|
||||
get adminApi() { return adminApi },
|
||||
get adminApp() { return adminApp },
|
||||
get nodeApp() { return nodeApp },
|
||||
|
||||
189
packages/node_modules/@node-red/runtime/lib/telemetry/index.js
vendored
Normal file
189
packages/node_modules/@node-red/runtime/lib/telemetry/index.js
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs/promises')
|
||||
const semver = require('semver')
|
||||
const cronosjs = require('cronosjs')
|
||||
|
||||
const METRICS_DIR = path.join(__dirname, 'metrics')
|
||||
const INITIAL_PING_DELAY = 1000 * 60 * 30 // 30 minutes from startup
|
||||
|
||||
let runtime
|
||||
|
||||
let scheduleTask
|
||||
|
||||
async function gather () {
|
||||
let metricFiles = await fs.readdir(METRICS_DIR)
|
||||
metricFiles = metricFiles.filter(name => /^\d+-.*\.js$/.test(name))
|
||||
metricFiles.sort()
|
||||
|
||||
const metrics = {}
|
||||
|
||||
for (let i = 0, l = metricFiles.length; i < l; i++) {
|
||||
const metricModule = require(path.join(METRICS_DIR, metricFiles[i]))
|
||||
let result = metricModule(runtime)
|
||||
if (!!result && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
|
||||
result = await result
|
||||
}
|
||||
const keys = Object.keys(result)
|
||||
keys.forEach(key => {
|
||||
const keyParts = key.split('.')
|
||||
let p = metrics
|
||||
keyParts.forEach((part, index) => {
|
||||
if (index < keyParts.length - 1) {
|
||||
if (!p[part]) {
|
||||
p[part] = {}
|
||||
}
|
||||
p = p[part]
|
||||
} else {
|
||||
p[part] = result[key]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
async function report () {
|
||||
if (!isTelemetryEnabled()) {
|
||||
return
|
||||
}
|
||||
// If enabled, gather metrics
|
||||
const metrics = await gather()
|
||||
console.log(JSON.stringify(metrics, null, 2))
|
||||
|
||||
// Post metrics to endpoint - handle any error silently
|
||||
|
||||
const { got } = await import('got')
|
||||
runtime.log.debug('Sending telemetry')
|
||||
const response = await got.post('https://telemetry.nodered.org/ping', {
|
||||
json: metrics,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'User-Agent': `Node-RED/${runtime.settings.version}`
|
||||
}
|
||||
}).json().catch(err => {
|
||||
// swallow errors
|
||||
runtime.log.debug('Failed to send telemetry: ' + err.toString())
|
||||
})
|
||||
// Example response:
|
||||
// { 'node-red': { latest: '4.0.9', next: '4.1.0-beta.1.9' } }
|
||||
runtime.log.debug(`Telemetry response: ${JSON.stringify(response)}`)
|
||||
// Get response from endpoint
|
||||
if (response?.['node-red']) {
|
||||
const currentVersion = metrics.env['node-red']
|
||||
if (semver.valid(currentVersion)) {
|
||||
const latest = response['node-red'].latest
|
||||
const next = response['node-red'].next
|
||||
let updatePayload
|
||||
if (semver.lt(currentVersion, latest)) {
|
||||
// Case one: current < latest
|
||||
runtime.log.info(`A new version of Node-RED is available: ${latest}`)
|
||||
updatePayload = { version: latest }
|
||||
} else if (semver.gt(currentVersion, latest) && semver.lt(currentVersion, next)) {
|
||||
// Case two: current > latest && current < next
|
||||
runtime.log.info(`A new beta version of Node-RED is available: ${next}`)
|
||||
updatePayload = { version: next }
|
||||
}
|
||||
|
||||
if (updatePayload && isUpdateNotificationEnabled()) {
|
||||
runtime.events.emit("runtime-event",{id:"update-available", payload: updatePayload, retain: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isTelemetryEnabled () {
|
||||
// If NODE_RED_DISABLE_TELEMETRY was set, or --no-telemetry was specified,
|
||||
// the settings object will have been updated to disable telemetry explicitly
|
||||
|
||||
// If there are no telemetry settings then the user has not had a chance
|
||||
// to opt out yet - so keep it disabled until they do
|
||||
|
||||
const telemetrySettings = runtime.settings.get('telemetry')
|
||||
const runtimeTelemetryEnabled = runtime.settings.get('telemetryEnabled')
|
||||
|
||||
if (telemetrySettings === undefined && runtimeTelemetryEnabled === undefined) {
|
||||
// No telemetry settings - so keep it disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If there are telemetry settings, use what it says
|
||||
if (telemetrySettings && telemetrySettings.enabled !== undefined) {
|
||||
return telemetrySettings.enabled
|
||||
}
|
||||
|
||||
// User has made a choice; defer to that
|
||||
if (runtimeTelemetryEnabled !== undefined) {
|
||||
return runtimeTelemetryEnabled
|
||||
}
|
||||
|
||||
// At this point, we have no sign the user has consented to telemetry, so
|
||||
// keep disabled - but return undefined as a false-like value to distinguish
|
||||
// it from the explicit disable above
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isUpdateNotificationEnabled () {
|
||||
const telemetrySettings = runtime.settings.get('telemetry') || {}
|
||||
return telemetrySettings.updateNotification !== false
|
||||
}
|
||||
/**
|
||||
* Start the telemetry schedule
|
||||
*/
|
||||
function startTelemetry () {
|
||||
if (scheduleTask) {
|
||||
// Already scheduled - nothing left to do
|
||||
return
|
||||
}
|
||||
|
||||
const pingTime = new Date(Date.now() + INITIAL_PING_DELAY)
|
||||
const pingMinutes = '*'//pingTime.getMinutes()
|
||||
const pingHours = '*'//pingTime.getHours()
|
||||
const pingSchedule = `${pingMinutes} ${pingHours} * * *`
|
||||
runtime.log.debug(`Telemetry enabled. Schedule: ${pingSchedule}`)
|
||||
|
||||
scheduleTask = cronosjs.scheduleTask(pingSchedule, () => {
|
||||
report()
|
||||
})
|
||||
}
|
||||
|
||||
function stopTelemetry () {
|
||||
if (scheduleTask) {
|
||||
runtime.log.debug(`Telemetry disabled`)
|
||||
scheduleTask.stop()
|
||||
scheduleTask = null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: (_runtime) => {
|
||||
runtime = _runtime
|
||||
if (isTelemetryEnabled()) {
|
||||
startTelemetry()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Enable telemetry via user opt-in in the editor
|
||||
*/
|
||||
enable: () => {
|
||||
if (runtime.settings.available()) {
|
||||
runtime.settings.set('telemetryEnabled', true)
|
||||
}
|
||||
startTelemetry()
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable telemetry via user opt-in in the editor
|
||||
*/
|
||||
disable: () => {
|
||||
if (runtime.settings.available()) {
|
||||
runtime.settings.set('telemetryEnabled', false)
|
||||
}
|
||||
stopTelemetry()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get telemetry enabled status
|
||||
* @returns {boolean} true if telemetry is enabled, false if disabled, undefined if not set
|
||||
*/
|
||||
isEnabled: isTelemetryEnabled
|
||||
}
|
||||
5
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js
vendored
Normal file
5
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/01-core.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = (runtime) => {
|
||||
return {
|
||||
instanceId: runtime.settings.get('instanceId')
|
||||
}
|
||||
}
|
||||
9
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js
vendored
Normal file
9
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/02-os.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const os = require('os')
|
||||
|
||||
module.exports = (_) => {
|
||||
return {
|
||||
'os.type': os.type(),
|
||||
'os.release': os.release(),
|
||||
'os.arch': os.arch()
|
||||
}
|
||||
}
|
||||
8
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js
vendored
Normal file
8
packages/node_modules/@node-red/runtime/lib/telemetry/metrics/03-env.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
const process = require('process')
|
||||
|
||||
module.exports = async (runtime) => {
|
||||
return {
|
||||
'env.nodejs': process.version.replace(/^v/, ''),
|
||||
'env.node-red': runtime.settings.version
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,11 @@
|
||||
"@node-red/util": "4.1.0-beta.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"clone": "2.1.2",
|
||||
"cronosjs": "1.7.1",
|
||||
"express": "4.21.2",
|
||||
"fs-extra": "11.3.0",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"rfdc": "^1.3.1"
|
||||
"rfdc": "^1.3.1",
|
||||
"semver": "7.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
8
packages/node_modules/node-red/red.js
vendored
8
packages/node_modules/node-red/red.js
vendored
@@ -63,7 +63,8 @@ var knownOpts = {
|
||||
"verbose": Boolean,
|
||||
"safe": Boolean,
|
||||
"version": Boolean,
|
||||
"define": [String, Array]
|
||||
"define": [String, Array],
|
||||
"no-telemetry": Boolean
|
||||
};
|
||||
var shortHands = {
|
||||
"?":["--help"],
|
||||
@@ -97,6 +98,7 @@ if (parsedArgs.help) {
|
||||
console.log(" --safe enable safe mode");
|
||||
console.log(" -D, --define X=Y overwrite value in settings file");
|
||||
console.log(" --version show version information");
|
||||
console.log(" --no-telemetry do not share usage data with the Node-RED project");
|
||||
console.log(" -?, --help show this help");
|
||||
console.log(" admin <command> run an admin command");
|
||||
console.log("");
|
||||
@@ -222,6 +224,10 @@ if (process.env.NODE_RED_ENABLE_TOURS) {
|
||||
settings.editorTheme.tours = !/^false$/i.test(process.env.NODE_RED_ENABLE_TOURS);
|
||||
}
|
||||
|
||||
if (parsedArgs.telemetry === false || process.env.NODE_RED_DISABLE_TELEMETRY) {
|
||||
settings.telemetry = settings.telemetry || {};
|
||||
settings.telemetry.enabled = false;
|
||||
}
|
||||
|
||||
var defaultServerSettings = {
|
||||
"x-powered-by": false
|
||||
|
||||
Reference in New Issue
Block a user