Files
n8n-nodes-gwezz-changdunovel/node_modules/n8n-workflow/dist/esm/telemetry-helpers.js
2025-10-26 23:10:15 +08:00

553 lines
25 KiB
JavaScript

import { AGENT_LANGCHAIN_NODE_TYPE, AGENT_TOOL_LANGCHAIN_NODE_TYPE, AI_TRANSFORM_NODE_TYPE, CHAIN_LLM_LANGCHAIN_NODE_TYPE, CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, CODE_NODE_TYPE, EVALUATION_NODE_TYPE, EVALUATION_TRIGGER_NODE_TYPE, EXECUTE_WORKFLOW_NODE_TYPE, FREE_AI_CREDITS_ERROR_TYPE, FREE_AI_CREDITS_USED_ALL_CREDITS_ERROR_CODE, FROM_AI_AUTO_GENERATED_MARKER, HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_LANGCHAIN_NODE_TYPE, LANGCHAIN_CUSTOM_TOOLS, MERGE_NODE_TYPE, OPEN_AI_API_CREDENTIAL_TYPE, OPENAI_LANGCHAIN_NODE_TYPE, STICKY_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, } from './constants';
import { ApplicationError } from '@n8n/errors';
import { NodeConnectionTypes } from './interfaces';
import { getNodeParameters } from './node-helpers';
import { jsonParse } from './utils';
import { DEFAULT_EVALUATION_METRIC } from './evaluation-helpers';
const isNodeApiError = (error) => typeof error === 'object' && error !== null && 'name' in error && error?.name === 'NodeApiError';
export function getNodeTypeForName(workflow, nodeName) {
return workflow.nodes.find((node) => node.name === nodeName);
}
export function isNumber(value) {
return typeof value === 'number';
}
const countPlaceholders = (text) => {
const placeholder = /(\{[a-zA-Z0-9_]+\})/g;
let returnData = 0;
try {
const matches = text.matchAll(placeholder);
for (const _ of matches)
returnData++;
}
catch (error) { }
return returnData;
};
const countPlaceholdersInParameters = (parameters) => {
let returnData = 0;
for (const parameter of parameters) {
if (!parameter.value) {
//count parameters provided by model
returnData++;
}
else {
//check if any placeholders in user provided value
returnData += countPlaceholders(String(parameter.value));
}
}
return returnData;
};
function areOverlapping(topLeft, bottomRight, targetPos) {
return (targetPos[0] > topLeft[0] &&
targetPos[1] > topLeft[1] &&
targetPos[0] < bottomRight[0] &&
targetPos[1] < bottomRight[1]);
}
const URL_PARTS_REGEX = /(?<protocolPlusDomain>.*?\..*?)(?<pathname>\/.*)/;
export function getDomainBase(raw, urlParts = URL_PARTS_REGEX) {
try {
const url = new URL(raw);
return [url.protocol, url.hostname].join('//');
}
catch {
const match = urlParts.exec(raw);
if (!match?.groups?.protocolPlusDomain)
return '';
return match.groups.protocolPlusDomain;
}
}
function isSensitive(segment) {
if (/^v\d+$/.test(segment))
return false;
return /%40/.test(segment) || /\d/.test(segment) || /^[0-9A-F]{8}/i.test(segment);
}
export const ANONYMIZATION_CHARACTER = '*';
function sanitizeRoute(raw, check = isSensitive, char = ANONYMIZATION_CHARACTER) {
return raw
.split('/')
.map((segment) => (check(segment) ? char.repeat(segment.length) : segment))
.join('/');
}
/**
* Return pathname plus query string from URL, anonymizing IDs in route and query params.
*/
export function getDomainPath(raw, urlParts = URL_PARTS_REGEX) {
try {
const url = new URL(raw);
if (!url.hostname)
throw new ApplicationError('Malformed URL');
return sanitizeRoute(url.pathname);
}
catch {
const match = urlParts.exec(raw);
if (!match?.groups?.pathname)
return '';
// discard query string
const route = match.groups.pathname.split('?').shift();
return sanitizeRoute(route);
}
}
function getNumberOfItemsInRuns(runs) {
return runs.reduce((total, run) => {
const data = run.data ?? {};
let count = 0;
Object.keys(data).forEach((type) => {
const conn = data[type] ?? [];
conn.forEach((branch) => {
count += (branch ?? []).length;
});
});
return total + count;
}, 0);
}
export function generateNodesGraph(workflow, nodeTypes, options) {
const { runData } = options ?? {};
const nodeGraph = {
node_types: [],
node_connections: [],
nodes: {},
notes: {},
is_pinned: Object.keys(workflow.pinData ?? {}).length > 0,
};
const nameIndices = {};
const webhookNodeNames = [];
const evaluationTriggerNodeNames = [];
const nodes = (workflow.nodes ?? []).filter((node) => node.type === STICKY_NODE_TYPE);
const otherNodes = (workflow.nodes ?? []).filter((node) => node.type !== STICKY_NODE_TYPE);
nodes.forEach((stickyNote, index) => {
const stickyType = nodeTypes.getByNameAndVersion(STICKY_NODE_TYPE, stickyNote.typeVersion);
if (!stickyType) {
return;
}
let nodeParameters = {};
try {
nodeParameters =
getNodeParameters(stickyType.description.properties, stickyNote.parameters, true, false, stickyNote, stickyType.description) ?? {};
}
catch {
// prevent node param resolution from failing graph generation
}
const height = typeof nodeParameters.height === 'number' ? nodeParameters.height : 0;
const width = typeof nodeParameters.width === 'number' ? nodeParameters.width : 0;
const topLeft = stickyNote.position;
const bottomRight = [topLeft[0] + width, topLeft[1] + height];
const overlapping = Boolean(otherNodes.find((node) => areOverlapping(topLeft, bottomRight, node.position)));
nodeGraph.notes[index] = {
overlapping,
position: topLeft,
height,
width,
};
});
// eslint-disable-next-line complexity
otherNodes.forEach((node, index) => {
nodeGraph.node_types.push(node.type);
const nodeItem = {
id: node.id,
type: node.type,
version: node.typeVersion,
position: node.position,
};
const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType?.description?.communityNodePackageVersion) {
nodeItem.package_version = nodeType.description.communityNodePackageVersion;
}
if (runData?.[node.name]) {
const runs = runData[node.name] ?? [];
nodeItem.runs = runs.length;
nodeItem.items_total = getNumberOfItemsInRuns(runs);
}
if (options?.sourceInstanceId) {
nodeItem.src_instance_id = options.sourceInstanceId;
}
if (node.id && options?.nodeIdMap?.[node.id]) {
nodeItem.src_node_id = options.nodeIdMap[node.id];
}
if (node.type === AI_TRANSFORM_NODE_TYPE && options?.isCloudDeployment) {
nodeItem.prompts = { instructions: node.parameters.instructions };
}
else if (node.type === AGENT_LANGCHAIN_NODE_TYPE) {
nodeItem.agent = node.parameters.agent ?? 'toolsAgent';
if (node.typeVersion >= 2.1) {
const options = node.parameters?.options;
if (typeof options === 'object' &&
options &&
'enableStreaming' in options &&
options.enableStreaming === false) {
nodeItem.is_streaming = false;
}
else {
nodeItem.is_streaming = true;
}
}
}
else if (node.type === MERGE_NODE_TYPE) {
nodeItem.operation = node.parameters.mode;
if (options?.isCloudDeployment && node.parameters.mode === 'combineBySql') {
nodeItem.sql = node.parameters.query;
}
}
else if (node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion === 1) {
try {
nodeItem.domain = new URL(node.parameters.url).hostname;
}
catch {
nodeItem.domain = getDomainBase(node.parameters.url);
}
}
else if (node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion > 1) {
const { authentication } = node.parameters;
nodeItem.credential_type = {
none: 'none',
genericCredentialType: node.parameters.genericAuthType,
predefinedCredentialType: node.parameters.nodeCredentialType,
}[authentication];
nodeItem.credential_set = node.credentials ? Object.keys(node.credentials).length > 0 : false;
const { url } = node.parameters;
nodeItem.domain_base = getDomainBase(url);
nodeItem.domain_path = getDomainPath(url);
nodeItem.method = node.parameters.requestMethod;
}
else if (HTTP_REQUEST_TOOL_LANGCHAIN_NODE_TYPE === node.type) {
if (!nodeItem.toolSettings)
nodeItem.toolSettings = {};
nodeItem.toolSettings.url_type = 'other';
nodeItem.toolSettings.uses_auth = false;
nodeItem.toolSettings.placeholders = 0;
nodeItem.toolSettings.query_from_model_only = false;
nodeItem.toolSettings.headers_from_model_only = false;
nodeItem.toolSettings.body_from_model_only = false;
const toolUrl = node.parameters?.url ?? '';
nodeItem.toolSettings.placeholders += countPlaceholders(toolUrl);
const authType = node.parameters?.authentication ?? '';
if (authType && authType !== 'none') {
nodeItem.toolSettings.uses_auth = true;
}
if (toolUrl.startsWith('{') && toolUrl.endsWith('}')) {
nodeItem.toolSettings.url_type = 'any';
}
else if (toolUrl.includes('google.com')) {
nodeItem.toolSettings.url_type = 'google';
}
if (node.parameters?.sendBody) {
if (node.parameters?.specifyBody === 'model') {
nodeItem.toolSettings.body_from_model_only = true;
}
if (node.parameters?.jsonBody) {
nodeItem.toolSettings.placeholders += countPlaceholders(node.parameters?.jsonBody);
}
if (node.parameters?.parametersBody) {
const parameters = (node.parameters?.parametersBody)
.values;
nodeItem.toolSettings.placeholders += countPlaceholdersInParameters(parameters);
}
}
if (node.parameters?.sendHeaders) {
if (node.parameters?.specifyHeaders === 'model') {
nodeItem.toolSettings.headers_from_model_only = true;
}
if (node.parameters?.jsonHeaders) {
nodeItem.toolSettings.placeholders += countPlaceholders(node.parameters?.jsonHeaders);
}
if (node.parameters?.parametersHeaders) {
const parameters = (node.parameters?.parametersHeaders)
.values;
nodeItem.toolSettings.placeholders += countPlaceholdersInParameters(parameters);
}
}
if (node.parameters?.sendQuery) {
if (node.parameters?.specifyQuery === 'model') {
nodeItem.toolSettings.query_from_model_only = true;
}
if (node.parameters?.jsonQuery) {
nodeItem.toolSettings.placeholders += countPlaceholders(node.parameters?.jsonQuery);
}
if (node.parameters?.parametersQuery) {
const parameters = (node.parameters?.parametersQuery)
.values;
nodeItem.toolSettings.placeholders += countPlaceholdersInParameters(parameters);
}
}
}
else if (node.type === WEBHOOK_NODE_TYPE) {
webhookNodeNames.push(node.name);
const responseMode = node.parameters?.responseMode;
nodeItem.response_mode = typeof responseMode === 'string' ? responseMode : 'onReceived';
}
else if (node.type === CHAT_TRIGGER_NODE_TYPE) {
// Capture streaming response mode parameter
const options = node.parameters?.options;
if (typeof options === 'object' &&
options &&
'responseMode' in options &&
typeof options.responseMode === 'string') {
nodeItem.response_mode = options.responseMode;
}
// Capture public chat setting
const isPublic = node.parameters?.public;
if (typeof isPublic === 'boolean') {
nodeItem.public_chat = isPublic;
}
}
else if (node.type === EXECUTE_WORKFLOW_NODE_TYPE ||
node.type === WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE) {
if (node.parameters?.workflowId) {
nodeItem.workflow_id = node.parameters?.workflowId;
}
}
else if (node.type === EVALUATION_TRIGGER_NODE_TYPE) {
evaluationTriggerNodeNames.push(node.name);
}
else if (node.type === EVALUATION_NODE_TYPE &&
options?.isCloudDeployment &&
node.parameters?.operation === 'setMetrics') {
const metrics = node.parameters?.metrics;
// If metrics are not defined, it means the node is using preconfigured metric
if (!metrics) {
const predefinedMetricKey = node.parameters?.metric ?? DEFAULT_EVALUATION_METRIC;
nodeItem.metric_names = [predefinedMetricKey];
}
else {
nodeItem.metric_names = metrics.assignments?.map((metric) => metric.name);
}
}
else if (node.type === CODE_NODE_TYPE) {
const { language } = node.parameters;
nodeItem.language =
language === undefined
? 'javascript'
: language === 'python'
? 'python'
: language === 'pythonNative'
? 'pythonNative'
: 'unknown';
}
else {
try {
const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType) {
const nodeParameters = getNodeParameters(nodeType.description.properties, node.parameters, true, false, node, nodeType.description);
if (nodeParameters) {
const keys = [
'operation',
'resource',
'mode',
];
keys.forEach((key) => {
if (nodeParameters.hasOwnProperty(key)) {
nodeItem[key] = nodeParameters[key]?.toString();
}
});
}
}
}
catch (e) {
if (!(e instanceof Error &&
typeof e.message === 'string' &&
e.message.includes('Unrecognized node type'))) {
throw e;
}
}
}
if (options?.isCloudDeployment === true) {
if (node.type === OPENAI_LANGCHAIN_NODE_TYPE) {
nodeItem.prompts =
(node.parameters?.messages ?? {}).values ?? [];
}
if (node.type === AGENT_LANGCHAIN_NODE_TYPE || node.type === AGENT_TOOL_LANGCHAIN_NODE_TYPE) {
const prompts = {};
if (node.parameters?.text) {
prompts.text = node.parameters.text;
}
const nodeOptions = node.parameters?.options;
if (nodeOptions) {
const optionalMessagesKeys = [
'humanMessage',
'systemMessage',
'humanMessageTemplate',
'prefix',
'suffixChat',
'suffix',
'prefixPrompt',
'suffixPrompt',
];
for (const key of optionalMessagesKeys) {
if (nodeOptions[key]) {
prompts[key] = nodeOptions[key];
}
}
}
if (Object.keys(prompts).length) {
nodeItem.prompts = prompts;
}
}
if (node.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) {
nodeItem.prompts = ((node.parameters?.options ?? {})
.summarizationMethodAndPrompts ?? {}).values;
}
if (LANGCHAIN_CUSTOM_TOOLS.includes(node.type)) {
nodeItem.prompts = {
description: node.parameters?.description ?? '',
};
}
if (node.type === CHAIN_LLM_LANGCHAIN_NODE_TYPE) {
nodeItem.prompts =
(node.parameters?.messages ?? {}).messageValues ?? [];
}
if (node.type === MERGE_NODE_TYPE && node.parameters?.operation === 'combineBySql') {
nodeItem.sql = node.parameters?.query;
}
}
nodeGraph.nodes[index.toString()] = nodeItem;
nameIndices[node.name] = index.toString();
});
const getGraphConnectionItem = (startNode, connectionItem) => {
return { start: nameIndices[startNode], end: nameIndices[connectionItem.node] };
};
Object.keys(workflow.connections ?? []).forEach((nodeName) => {
const connections = workflow.connections?.[nodeName];
if (!connections) {
return;
}
Object.keys(connections).forEach((key) => {
connections[key].forEach((element) => {
(element ?? []).forEach((element2) => {
nodeGraph.node_connections.push(getGraphConnectionItem(nodeName, element2));
});
});
});
});
return { nodeGraph, nameIndices, webhookNodeNames, evaluationTriggerNodeNames };
}
export function extractLastExecutedNodeCredentialData(runData) {
const nodeCredentials = runData?.data?.executionData?.nodeExecutionStack?.[0]?.node?.credentials;
if (!nodeCredentials)
return null;
const credentialType = Object.keys(nodeCredentials)[0] ?? null;
if (!credentialType)
return null;
const { id } = nodeCredentials[credentialType];
if (!id)
return null;
return { credentialId: id, credentialType };
}
export const userInInstanceRanOutOfFreeAiCredits = (runData) => {
const credentials = extractLastExecutedNodeCredentialData(runData);
if (!credentials)
return false;
if (credentials.credentialType !== OPEN_AI_API_CREDENTIAL_TYPE)
return false;
const { error } = runData.data.resultData;
if (!isNodeApiError(error) || !error.messages[0])
return false;
const rawErrorResponse = error.messages[0].replace(`${error.httpCode} -`, '');
try {
const errorResponse = jsonParse(rawErrorResponse);
if (errorResponse?.error?.type === FREE_AI_CREDITS_ERROR_TYPE &&
errorResponse.error.code === FREE_AI_CREDITS_USED_ALL_CREDITS_ERROR_CODE) {
return true;
}
}
catch {
return false;
}
return false;
};
export function resolveAIMetrics(nodes, nodeTypes) {
const resolvedNodes = nodes
.map((x) => [x, nodeTypes.getByNameAndVersion(x.type, x.typeVersion)])
.filter((x) => !!x[1]?.description);
const aiNodeCount = resolvedNodes.reduce((acc, x) => acc + Number(x[1].description.codex?.categories?.includes('AI')), 0);
if (aiNodeCount === 0)
return {};
let fromAIOverrideCount = 0;
let fromAIExpressionCount = 0;
const tools = resolvedNodes.filter((node) => node[1].description.codex?.subcategories?.AI?.includes('Tools'));
for (const [node, _] of tools) {
// FlatMap to support values in resourceLocators
const values = Object.values(node.parameters).flatMap((param) => {
if (param && typeof param === 'object' && 'value' in param)
param = param.value;
return typeof param === 'string' ? param : [];
});
// Note that we don't match the i in `fromAI` to support lower case i (though we miss fromai)
const overrides = values.reduce((acc, value) => acc + Number(value.startsWith(`={{ ${FROM_AI_AUTO_GENERATED_MARKER} $fromA`)), 0);
fromAIOverrideCount += overrides;
// check for = to avoid scanning lengthy text fields
// this will re-count overrides
fromAIExpressionCount +=
values.reduce((acc, value) => acc + Number(value[0] === '=' && value.includes('$fromA', 2)), 0) - overrides;
}
return {
aiNodeCount,
aiToolCount: tools.length,
fromAIOverrideCount,
fromAIExpressionCount,
};
}
export function resolveVectorStoreMetrics(nodes, nodeTypes, run) {
const resolvedNodes = nodes
.map((x) => [x, nodeTypes.getByNameAndVersion(x.type, x.typeVersion)])
.filter((x) => !!x[1]?.description);
const vectorStores = resolvedNodes.filter((x) => x[1].description.codex?.categories?.includes('AI') &&
x[1].description.codex?.subcategories?.AI?.includes('Vector Stores'));
if (vectorStores.length === 0)
return {};
const runData = run?.data?.resultData?.runData;
const succeededVectorStores = vectorStores.filter((x) => runData?.[x[0].name]?.some((execution) => execution.executionStatus === 'success'));
const insertingVectorStores = succeededVectorStores.filter((x) => x[0].parameters?.mode === 'insert');
const retrievingVectorStores = succeededVectorStores.filter((x) => ['retrieve-as-tool', 'retrieve', 'load'].find((y) => y === x[0].parameters?.mode));
return {
insertedIntoVectorStore: insertingVectorStores.length > 0,
queriedDataFromVectorStore: retrievingVectorStores.length > 0,
};
}
/**
* Extract additional debug information if the last executed node was an agent node
*/
export function extractLastExecutedNodeStructuredOutputErrorInfo(workflow, nodeTypes, runData) {
const info = {};
if (runData?.data.resultData.error && runData.data.resultData.lastNodeExecuted) {
const lastNode = getNodeTypeForName(workflow, runData.data.resultData.lastNodeExecuted);
if (lastNode !== undefined) {
if (lastNode.type === AGENT_LANGCHAIN_NODE_TYPE && lastNode.parameters.hasOutputParser) {
// Add additional debug info for agent node structured output errors
const agentOutputError = runData.data.resultData.runData[lastNode.name]?.[0]?.error;
if (agentOutputError &&
agentOutputError.message === "Model output doesn't fit required format") {
info.output_parser_fail_reason = agentOutputError.context
?.outputParserFailReason;
}
if (workflow.connections) {
// Count connected tools
info.num_tools =
Object.keys(workflow.connections).filter((node) => workflow.connections[node]?.[NodeConnectionTypes.AiTool]?.[0]?.some((connectedNode) => connectedNode.node === lastNode.name))?.length ?? 0;
// Extract model name from the language model node if connected
const languageModelNodeName = Object.keys(workflow.connections).find((node) => workflow.connections[node]?.[NodeConnectionTypes.AiLanguageModel]?.[0]?.some((connectedNode) => connectedNode.node === lastNode.name));
if (languageModelNodeName) {
const languageModelNode = getNodeTypeForName(workflow, languageModelNodeName);
if (languageModelNode) {
const nodeType = nodeTypes.getByNameAndVersion(languageModelNode.type, languageModelNode.typeVersion);
if (nodeType) {
const nodeParameters = getNodeParameters(nodeType.description.properties, languageModelNode.parameters, true, false, languageModelNode, nodeType.description);
const modelNameKeys = ['model', 'modelName'];
for (const key of modelNameKeys) {
if (nodeParameters?.[key]) {
info.model_name =
typeof nodeParameters[key] === 'string'
? nodeParameters[key]
: nodeParameters[key].value;
if (info.model_name) {
break;
}
}
}
}
}
}
}
}
}
}
return info;
}
//# sourceMappingURL=telemetry-helpers.js.map