serverless/lib/utils/telemetry/generate-payload.js
2022-05-23 15:32:36 +02:00

295 lines
9.5 KiB
JavaScript

'use strict';
const path = require('path');
const crypto = require('crypto');
const _ = require('lodash');
const isPlainObject = require('type/plain-object/is');
const isObject = require('type/object/is');
const userConfig = require('@serverless/utils/config');
const getNotificationsMode = require('@serverless/utils/get-notifications-mode');
const log = require('@serverless/utils/log').log.get('telemetry');
const isStandalone = require('../is-standalone-executable');
const { getConfigurationValidationResult } = require('../../classes/config-schema-handler');
const { triggeredDeprecations } = require('../log-deprecation');
const isNpmGlobal = require('../npm-package/is-global');
const isLocallyInstalled = require('../../cli/is-locally-installed');
const ci = require('ci-info');
const AWS = require('aws-sdk');
const configValidationModeValues = new Set(['off', 'warn', 'error']);
const commandsReportingProjectId = new Set(['deploy', '', 'remove']);
const getServiceConfig = ({ configuration, options, variableSources }) => {
const providerName = isObject(configuration.provider)
? configuration.provider.name
: configuration.provider;
const isAwsProvider = providerName === 'aws';
const defaultRuntime = isAwsProvider
? configuration.provider.runtime || 'nodejs12.x'
: _.get(configuration, 'provider.runtime');
const functions = (() => {
if (isObject(configuration.functions)) return configuration.functions;
if (!Array.isArray(configuration.functions)) return {};
const result = {};
for (const functionsBlock of configuration.functions) Object.assign(result, functionsBlock);
return result;
})();
const resolveResourceTypes = (resources) => {
if (!isPlainObject(resources)) return [];
return [
...new Set(
Object.values(resources)
.map((resource) => {
const type = _.get(resource, 'Type');
if (typeof type !== 'string' || !type.includes('::')) return null;
const domain = type.slice(0, type.indexOf(':'));
return domain === 'AWS' ? type : domain;
})
.filter(Boolean)
),
];
};
const resources = (() => {
if (!isAwsProvider) return undefined;
return {
general: resolveResourceTypes(_.get(configuration.resources, 'Resources')),
};
})();
const plugins = configuration.plugins
? configuration.plugins.modules || configuration.plugins
: [];
const resolveUniqueParamsCount = (params) => {
if (!isPlainObject(params)) return 0;
return new Set(
Object.values(params)
.filter(isPlainObject)
.map((stageParams) => Object.keys(stageParams))
.reduce((acc, stageParamsKeys) => [...acc, ...stageParamsKeys], [])
).size;
};
const result = {
// TODO: Update when upgrading the default for next major
configValidationMode: configValidationModeValues.has(configuration.configValidationMode)
? configuration.configValidationMode
: 'warn',
provider: {
name: providerName,
runtime: defaultRuntime,
stage: options.stage || _.get(configuration, 'provider.stage') || 'dev',
region: isAwsProvider
? options.region || configuration.provider.region || 'us-east-1'
: _.get(configuration, 'provider.region'),
},
variableSources: variableSources ? Array.from(variableSources) : [],
plugins,
functions: Object.values(functions)
.map((functionConfig) => {
if (!functionConfig) return null;
const functionEvents = Array.isArray(functionConfig.events) ? functionConfig.events : [];
const functionRuntime = (() => {
if (functionConfig.image) return '$containerimage';
return functionConfig.runtime || defaultRuntime;
})();
return {
url: Boolean(functionConfig.url),
runtime: functionRuntime,
events: functionEvents.map((eventConfig) => ({
type: isObject(eventConfig) ? Object.keys(eventConfig)[0] || null : null,
})),
};
})
.filter(Boolean),
resources,
paramsCount: resolveUniqueParamsCount(configuration.params),
};
// We want to recognize types of constructs from `serverless-lift` plugin if possible
if (plugins.includes('serverless-lift') && _.isObject(configuration.constructs)) {
result.constructs = Object.values(configuration.constructs)
.map((construct) => {
if (_.isObject(construct) && construct.type != null) {
return { type: construct.type };
}
return null;
})
.filter(Boolean);
}
return result;
};
// This method is explicitly kept as synchronous. The reason for it being the fact that it needs to
// be executed in such manner due to its use in `process.on('SIGINT')` handler.
module.exports = ({
command,
options,
commandSchema,
serviceDir,
configuration,
serverless,
commandUsage,
variableSources,
}) => {
let commandDurationMs;
if (EvalError.$serverlessCommandStartTime) {
const diff = process.hrtime(EvalError.$serverlessCommandStartTime);
// First element is in seconds and second in nanoseconds
commandDurationMs = Math.floor(diff[0] * 1000 + diff[1] / 1000000);
}
let timezone;
try {
timezone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch {
// Pass silently
}
const ciName = (() => {
if (process.env.SERVERLESS_CI_CD) {
return 'Serverless CI/CD';
}
if (process.env.SEED_APP_NAME) {
return 'Seed';
}
if (ci.isCI) {
if (ci.name) {
return ci.name;
}
return 'unknown';
}
return null;
})();
const userId = (() => {
// In this situation deployment relies on existence on company-wide access key
// and `userId` from config does not matter
if (process.env.SERVERLESS_ACCESS_KEY) {
return null;
}
return userConfig.get('userId');
})();
const usedVersions = (() => {
return {
'serverless': require('../../../package').version,
'@serverless/dashboard-plugin': require('@serverless/dashboard-plugin/package').version,
};
})();
// We only consider options that are present in command schema
const availableOptionNames = new Set(Object.keys(commandSchema.options));
const commandOptionNames = Object.keys(options).filter((x) => availableOptionNames.has(x));
const payload = {
ciName,
isTtyTerminal: process.stdin.isTTY && process.stdout.isTTY,
cliName: 'serverless',
command,
commandOptionNames,
console: {
userId,
},
dashboard: {
userId,
},
firstLocalInstallationTimestamp: userConfig.get('meta.created_at'),
frameworkLocalUserId: userConfig.get('frameworkId'),
installationType: (() => {
if (isStandalone) {
if (process.platform === 'win32') return 'global:standalone:windows';
return 'global:standalone:other';
}
if (!isLocallyInstalled) {
return isNpmGlobal() ? 'global:npm' : 'global:other';
}
if (EvalError.$serverlessInitInstallationVersion) return 'local:fallback';
return 'local:direct';
})(),
isAutoUpdateEnabled: Boolean(userConfig.get('autoUpdate.enabled')),
isUsingCompose: Boolean(process.env.SLS_COMPOSE),
notificationsMode: getNotificationsMode(),
timestamp: Date.now(),
timezone,
triggeredDeprecations: Array.from(triggeredDeprecations),
versions: usedVersions,
};
if (commandDurationMs != null) {
payload.commandDurationMs = commandDurationMs;
}
if (configuration && commandSchema.serviceDependencyMode) {
const npmDependencies = (() => {
const pkgJson = (() => {
try {
return require(path.resolve(serviceDir, 'package.json'));
} catch (error) {
return null;
}
})();
if (!pkgJson) return [];
return Array.from(
new Set([
...Object.keys(pkgJson.dependencies || {}),
...Object.keys(pkgJson.optionalDependencies || {}),
...Object.keys(pkgJson.devDependencies || {}),
])
);
})();
const providerName = isObject(configuration.provider)
? configuration.provider.name
: configuration.provider;
const isAwsProvider = providerName === 'aws';
payload.hasLocalCredentials = isAwsProvider && Boolean(new AWS.Config().credentials);
payload.npmDependencies = npmDependencies;
payload.config = getServiceConfig({ configuration, options, variableSources });
payload.isConfigValid = getConfigurationValidationResult(configuration);
payload.dashboard.orgUid =
serverless && serverless.isDashboardEnabled ? serverless.service.orgUid : undefined;
if (_.get(serverless, 'console.isEnabled')) {
payload.console.orgUid = serverless.console.orgId;
payload.console.extensionVersion = serverless.console._usedExtensionLayerVersionPostfix;
}
if (isAwsProvider && serverless && commandsReportingProjectId.has(command)) {
const serviceName = isObject(configuration.service)
? configuration.service.name
: configuration.service;
const accountId = serverless && serverless.getProvider('aws').accountId;
if (serviceName && accountId) {
payload.projectId = crypto
.createHash('sha256')
.update(`${serviceName}-${accountId}`)
.digest('base64');
}
}
if (isAwsProvider && serverless && (command === 'deploy' || command === '')) {
payload.didCreateService = Boolean(
serverless && serverless.getProvider('aws').didCreateService
);
}
}
if (commandUsage) {
payload.commandUsage = commandUsage;
}
log.debug('payload %o', payload);
return payload;
};