mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat: Add more telemetry to free AI credits feature (no-changelog) (#12493)
This commit is contained in:
@@ -95,3 +95,7 @@ export const AI_TRANSFORM_JS_CODE = 'jsCode';
|
||||
* in `cli` package.
|
||||
*/
|
||||
export const TRIMMED_TASK_DATA_CONNECTIONS_KEY = '__isTrimmedManualExecutionDataItem';
|
||||
|
||||
export const OPEN_AI_API_CREDENTIAL_TYPE = 'openAiApi';
|
||||
export const FREE_AI_CREDITS_ERROR_TYPE = 'free_ai_credits_request_error';
|
||||
export const FREE_AI_CREDITS_USED_ALL_CREDITS_ERROR_CODE = 400;
|
||||
|
||||
@@ -4,16 +4,20 @@ import {
|
||||
CHAIN_LLM_LANGCHAIN_NODE_TYPE,
|
||||
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
||||
EXECUTE_WORKFLOW_NODE_TYPE,
|
||||
FREE_AI_CREDITS_ERROR_TYPE,
|
||||
FREE_AI_CREDITS_USED_ALL_CREDITS_ERROR_CODE,
|
||||
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 './errors/application.error';
|
||||
import type { NodeApiError } from './errors/node-api.error';
|
||||
import type {
|
||||
IConnection,
|
||||
INode,
|
||||
@@ -29,6 +33,10 @@ import type {
|
||||
IRun,
|
||||
} from './Interfaces';
|
||||
import { getNodeParameters } from './NodeHelpers';
|
||||
import { jsonParse } from './utils';
|
||||
|
||||
const isNodeApiError = (error: unknown): error is NodeApiError =>
|
||||
typeof error === 'object' && error !== null && 'name' in error && error?.name === 'NodeApiError';
|
||||
|
||||
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
|
||||
return workflow.nodes.find((node) => node.name === nodeName);
|
||||
@@ -489,3 +497,31 @@ export function extractLastExecutedNodeCredentialData(
|
||||
|
||||
return { credentialId: id, credentialType };
|
||||
}
|
||||
|
||||
export const userInInstanceRanOutOfFreeAiCredits = (runData: IRun): boolean => {
|
||||
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<{ error: { code: number; type: string } }>(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;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mock } from 'jest-mock-extended';
|
||||
import { v5 as uuidv5, v3 as uuidv3, v4 as uuidv4, v1 as uuidv1 } from 'uuid';
|
||||
|
||||
import { STICKY_NODE_TYPE } from '@/Constants';
|
||||
import { ApplicationError } from '@/errors';
|
||||
import { ApplicationError, ExpressionError, NodeApiError } from '@/errors';
|
||||
import type { IRun, IRunData } from '@/Interfaces';
|
||||
import { NodeConnectionType, type IWorkflowBase } from '@/Interfaces';
|
||||
import * as nodeHelpers from '@/NodeHelpers';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
generateNodesGraph,
|
||||
getDomainBase,
|
||||
getDomainPath,
|
||||
userInInstanceRanOutOfFreeAiCredits,
|
||||
} from '@/TelemetryHelpers';
|
||||
import { randomInt } from '@/utils';
|
||||
|
||||
@@ -930,6 +931,227 @@ describe('extractLastExecutedNodeCredentialData', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('userInInstanceRanOutOfFreeAiCredits', () => {
|
||||
it('should return false if could not find node credentials', () => {
|
||||
const runData = {
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
data: {
|
||||
startData: {
|
||||
destinationNode: 'OpenAI',
|
||||
runNodeFilter: ['OpenAI'],
|
||||
},
|
||||
executionData: {
|
||||
nodeExecutionStack: [{ node: { credentials: {} } }],
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
lastNodeExecuted: 'OpenAI',
|
||||
error: new NodeApiError(
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'OpenAI',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
parameters: {},
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
message: `400 - ${JSON.stringify({
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 200,
|
||||
},
|
||||
})}`,
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpCode: '400',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
} as unknown as IRun;
|
||||
|
||||
expect(userInInstanceRanOutOfFreeAiCredits(runData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if could not credential type it is not openAiApi', () => {
|
||||
const runData = {
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
data: {
|
||||
startData: {
|
||||
destinationNode: 'OpenAI',
|
||||
runNodeFilter: ['OpenAI'],
|
||||
},
|
||||
executionData: {
|
||||
nodeExecutionStack: [{ node: { credentials: { jiraApi: { id: 'nhu-l8E4hX' } } } }],
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
lastNodeExecuted: 'OpenAI',
|
||||
error: new NodeApiError(
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'OpenAI',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
parameters: {},
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
message: `400 - ${JSON.stringify({
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 200,
|
||||
},
|
||||
})}`,
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpCode: '400',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
} as unknown as IRun;
|
||||
|
||||
expect(userInInstanceRanOutOfFreeAiCredits(runData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if error is not NodeApiError', () => {
|
||||
const runData = {
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
data: {
|
||||
startData: {
|
||||
destinationNode: 'OpenAI',
|
||||
runNodeFilter: ['OpenAI'],
|
||||
},
|
||||
executionData: {
|
||||
nodeExecutionStack: [{ node: { credentials: { openAiApi: { id: 'nhu-l8E4hX' } } } }],
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
lastNodeExecuted: 'OpenAI',
|
||||
error: new ExpressionError('error'),
|
||||
},
|
||||
},
|
||||
} as unknown as IRun;
|
||||
|
||||
expect(userInInstanceRanOutOfFreeAiCredits(runData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if error is not a free ai credit error', () => {
|
||||
const runData = {
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
data: {
|
||||
startData: {
|
||||
destinationNode: 'OpenAI',
|
||||
runNodeFilter: ['OpenAI'],
|
||||
},
|
||||
executionData: {
|
||||
nodeExecutionStack: [{ node: { credentials: { openAiApi: { id: 'nhu-l8E4hX' } } } }],
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
lastNodeExecuted: 'OpenAI',
|
||||
error: new NodeApiError(
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'OpenAI',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
parameters: {},
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
message: `400 - ${JSON.stringify({
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'error_type',
|
||||
code: 200,
|
||||
},
|
||||
})}`,
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'error_type',
|
||||
code: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpCode: '400',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
} as unknown as IRun;
|
||||
|
||||
expect(userInInstanceRanOutOfFreeAiCredits(runData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the user has ran out of free AI credits', () => {
|
||||
const runData = {
|
||||
status: 'error',
|
||||
mode: 'manual',
|
||||
data: {
|
||||
startData: {
|
||||
destinationNode: 'OpenAI',
|
||||
runNodeFilter: ['OpenAI'],
|
||||
},
|
||||
executionData: {
|
||||
nodeExecutionStack: [{ node: { credentials: { openAiApi: { id: 'nhu-l8E4hX' } } } }],
|
||||
},
|
||||
resultData: {
|
||||
runData: {},
|
||||
lastNodeExecuted: 'OpenAI',
|
||||
error: new NodeApiError(
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'OpenAI',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
parameters: {},
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
message: `400 - ${JSON.stringify({
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 400,
|
||||
},
|
||||
})}`,
|
||||
error: {
|
||||
message: 'error message',
|
||||
type: 'free_ai_credits_request_error',
|
||||
code: 400,
|
||||
},
|
||||
},
|
||||
{
|
||||
httpCode: '400',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
} as unknown as IRun;
|
||||
|
||||
expect(userInInstanceRanOutOfFreeAiCredits(runData)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
function validUrls(idMaker: typeof alphanumericId | typeof email, char = CHAR) {
|
||||
const firstId = idMaker();
|
||||
const secondId = idMaker();
|
||||
|
||||
Reference in New Issue
Block a user