mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
chore(core): Send a telementry event if a node outputs non json data (#16558)
Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
committed by
GitHub
parent
297d3001c0
commit
6edde7fb66
@@ -61,6 +61,7 @@ import PCancelable from 'p-cancelable';
|
||||
import { ErrorReporter } from '@/errors/error-reporter';
|
||||
import { WorkflowHasIssuesError } from '@/errors/workflow-has-issues.error';
|
||||
import * as NodeExecuteFunctions from '@/node-execute-functions';
|
||||
import { isJsonCompatible } from '@/utils/is-json-compatible';
|
||||
|
||||
import { ExecuteContext, PollContext } from './node-execution-context';
|
||||
import {
|
||||
@@ -1193,6 +1194,27 @@ export class WorkflowExecute {
|
||||
: await nodeType.execute.call(context);
|
||||
}
|
||||
|
||||
// If data is not json compatible then log it as incorrect output
|
||||
// Does not block the execution from continuing
|
||||
const jsonCompatibleResult = isJsonCompatible(data);
|
||||
if (!jsonCompatibleResult.isValid) {
|
||||
Container.get(ErrorReporter).error(
|
||||
new UnexpectedError('node execution output incorrect data'),
|
||||
{
|
||||
extra: {
|
||||
nodeName: node.name,
|
||||
nodeType: node.type,
|
||||
nodeVersion: node.typeVersion,
|
||||
workflowId: workflow.id,
|
||||
workflowName: workflow.name ?? 'Unnamed workflow',
|
||||
executionId: this.additionalData.executionId ?? 'unsaved-execution',
|
||||
errorPath: jsonCompatibleResult.errorPath,
|
||||
errorMessage: jsonCompatibleResult.errorMessage,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const closeFunctionsResults = await Promise.allSettled(
|
||||
closeFunctions.map(async (fn) => await fn()),
|
||||
);
|
||||
|
||||
134
packages/core/src/utils/__tests__/is-json-compatible.test.ts
Normal file
134
packages/core/src/utils/__tests__/is-json-compatible.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { isJsonCompatible } from '../is-json-compatible';
|
||||
|
||||
describe('isJsonCompatible', () => {
|
||||
type CircularReferenceObject = { self: CircularReferenceObject };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const circularReferencedObject: CircularReferenceObject = {} as any;
|
||||
circularReferencedObject.self = circularReferencedObject;
|
||||
|
||||
type CircularReferencedArray = CircularReferencedArray[];
|
||||
const circularReferencedArray: CircularReferencedArray = [];
|
||||
circularReferencedArray.push(circularReferencedArray);
|
||||
|
||||
type CircularReferencedArrayInObject = { cycle: CircularReferencedArrayInObject[] };
|
||||
const temp: CircularReferencedArrayInObject[] = [];
|
||||
const circularReferencedArrayInObject = { cycle: temp };
|
||||
temp.push(circularReferencedArrayInObject);
|
||||
|
||||
type CircularReferencedObjectInArray = Array<{ cycle: CircularReferencedObjectInArray }>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const circularReferencedObjectInArray: CircularReferencedObjectInArray = [] as any;
|
||||
circularReferencedObjectInArray.push({ cycle: circularReferencedObjectInArray });
|
||||
|
||||
test.each([
|
||||
{
|
||||
name: 'new Date()',
|
||||
value: { date: new Date() },
|
||||
errorPath: 'value.date',
|
||||
errorMessage: 'has non-plain prototype (Date)',
|
||||
},
|
||||
{
|
||||
name: 'a function',
|
||||
value: { fn: () => {} },
|
||||
errorPath: 'value.fn',
|
||||
errorMessage: 'is a function, which is not JSON-compatible',
|
||||
},
|
||||
{
|
||||
name: 'a circular referenced object',
|
||||
value: circularReferencedObject,
|
||||
errorPath: 'value.self',
|
||||
errorMessage: 'contains a circular reference',
|
||||
},
|
||||
{
|
||||
name: 'a circular referenced array',
|
||||
value: circularReferencedArray,
|
||||
errorPath: 'value[0]',
|
||||
errorMessage: 'contains a circular reference',
|
||||
},
|
||||
{
|
||||
name: 'an array in a object referencing the object',
|
||||
value: circularReferencedArrayInObject,
|
||||
errorPath: 'value.cycle[0]',
|
||||
errorMessage: 'contains a circular reference',
|
||||
},
|
||||
{
|
||||
name: 'an object in a array referencing the array',
|
||||
value: circularReferencedObjectInArray,
|
||||
errorPath: 'value[0].cycle',
|
||||
errorMessage: 'contains a circular reference',
|
||||
},
|
||||
{
|
||||
name: 'a symbol',
|
||||
value: { symbol: Symbol() },
|
||||
errorPath: 'value.symbol',
|
||||
errorMessage: 'is a symbol, which is not JSON-compatible',
|
||||
},
|
||||
{
|
||||
name: 'a bigint',
|
||||
value: { bigint: BigInt(1) },
|
||||
errorPath: 'value.bigint',
|
||||
errorMessage: 'is a bigint, which is not JSON-compatible',
|
||||
},
|
||||
{
|
||||
name: 'a Set',
|
||||
value: { bigint: new Set() },
|
||||
errorPath: 'value.bigint',
|
||||
errorMessage: 'has non-plain prototype (Set)',
|
||||
},
|
||||
{
|
||||
name: 'Infinity',
|
||||
value: { infinity: Infinity },
|
||||
errorPath: 'value.infinity',
|
||||
errorMessage: 'is Infinity, which is not JSON-compatible',
|
||||
},
|
||||
{
|
||||
name: 'NaN',
|
||||
value: { nan: NaN },
|
||||
errorPath: 'value.nan',
|
||||
errorMessage: 'is NaN, which is not JSON-compatible',
|
||||
},
|
||||
{
|
||||
name: 'undefined',
|
||||
value: { undefined },
|
||||
errorPath: 'value.undefined',
|
||||
errorMessage: 'is of unknown type (undefined) with value undefined',
|
||||
},
|
||||
{
|
||||
name: 'an object with symbol keys',
|
||||
value: { [Symbol.for('key')]: 1 },
|
||||
errorPath: 'value.Symbol(key)',
|
||||
errorMessage: 'has a symbol key (Symbol(key)), which is not JSON-compatible',
|
||||
},
|
||||
])('returns invalid for "$name"', ({ value, errorPath, errorMessage }) => {
|
||||
const result = isJsonCompatible(value);
|
||||
|
||||
if (result.isValid) {
|
||||
fail('expected result to be invalid');
|
||||
}
|
||||
|
||||
expect(result.errorPath).toBe(errorPath);
|
||||
expect(result.errorMessage).toBe(errorMessage);
|
||||
});
|
||||
|
||||
const objectRef = {};
|
||||
test.each([
|
||||
{ name: 'null', value: { null: null } },
|
||||
{ name: 'an array of primitives', value: { array: [1, 'string', true, false] } },
|
||||
{
|
||||
name: 'an object without a prototype chain',
|
||||
value: { objectWithoutPrototype: Object.create(null) },
|
||||
},
|
||||
{
|
||||
name: 'repeated objects references in an array that are not circular',
|
||||
value: { array: [objectRef, objectRef] },
|
||||
},
|
||||
{
|
||||
name: 'repeated objects references in an object that are not circular',
|
||||
value: { array: { object1: objectRef, object2: objectRef } },
|
||||
},
|
||||
])('returns valid for "$name"', ({ value }) => {
|
||||
const result = isJsonCompatible(value);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
106
packages/core/src/utils/is-json-compatible.ts
Normal file
106
packages/core/src/utils/is-json-compatible.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable complexity */
|
||||
const check = (
|
||||
val: unknown,
|
||||
path = 'value',
|
||||
stack: Set<unknown> = new Set(),
|
||||
): { isValid: true } | { isValid: false; errorPath: string; errorMessage: string } => {
|
||||
const type = typeof val;
|
||||
|
||||
if (val === null || type === 'boolean' || type === 'string') {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
if (!Number.isFinite(val)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
errorMessage: `is ${val as number}, which is not JSON-compatible`,
|
||||
};
|
||||
}
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
if (type === 'function' || type === 'symbol' || type === 'bigint') {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
errorMessage: `is a ${type}, which is not JSON-compatible`,
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
if (stack.has(val)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
errorMessage: 'contains a circular reference',
|
||||
};
|
||||
}
|
||||
stack.add(val);
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
const result = check(val[i], `${path}[${i}]`, stack);
|
||||
if (!result.isValid) return result;
|
||||
}
|
||||
stack.delete(val);
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
if (stack.has(val)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
errorMessage: 'contains a circular reference',
|
||||
};
|
||||
}
|
||||
stack.add(val);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const proto = Object.getPrototypeOf(val);
|
||||
if (proto !== Object.prototype && proto !== null) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
errorMessage: `has non-plain prototype (${proto?.constructor?.name || 'unknown'})`,
|
||||
};
|
||||
}
|
||||
for (const key of Reflect.ownKeys(val as object)) {
|
||||
if (typeof key === 'symbol') {
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: `${path}.${key.toString()}`,
|
||||
errorMessage: `has a symbol key (${String(key)}), which is not JSON-compatible`,
|
||||
};
|
||||
}
|
||||
|
||||
const subVal = (val as Record<string, unknown>)[key];
|
||||
const result = check(subVal, `${path}.${key}`, stack);
|
||||
if (!result.isValid) return result;
|
||||
}
|
||||
stack.delete(val);
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
errorPath: path,
|
||||
errorMessage: `is of unknown type (${type}) with value ${JSON.stringify(val)}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This function checks if a value matches JSON data type restrictions.
|
||||
* @param value
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isJsonCompatible(value: unknown):
|
||||
| { isValid: true }
|
||||
| {
|
||||
isValid: false;
|
||||
errorPath: string;
|
||||
errorMessage: string;
|
||||
} {
|
||||
return check(value);
|
||||
}
|
||||
Reference in New Issue
Block a user