feat: Add vector store telemetry events to Manual workflow exec finished event (no-changelog) (#16280)

This commit is contained in:
Charlie Kolb
2025-06-13 17:07:45 +02:00
committed by GitHub
parent d0a313aa1c
commit f92e1ea8e4
3 changed files with 260 additions and 0 deletions

View File

@@ -731,6 +731,7 @@ export class TelemetryEventRelay extends EventRelay {
is_managed: false,
eval_rows_left: null,
...TelemetryHelpers.resolveAIMetrics(workflow.nodes, this.nodeTypes),
...TelemetryHelpers.resolveVectorStoreMetrics(workflow.nodes, this.nodeTypes, runData),
};
if (!manualExecEventProperties.node_graph_string) {

View File

@@ -604,3 +604,43 @@ export function resolveAIMetrics(nodes: INode[], nodeTypes: INodeTypes): FromAIC
fromAIExpressionCount,
};
}
export type VectorStoreMetrics = {
insertedIntoVectorStore: boolean;
queriedDataFromVectorStore: boolean;
};
export function resolveVectorStoreMetrics(
nodes: INode[],
nodeTypes: INodeTypes,
run: IRun,
): VectorStoreMetrics | {} {
const resolvedNodes = nodes
.map((x) => [x, nodeTypes.getByNameAndVersion(x.type, x.typeVersion)] as const)
.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,
};
}

View File

@@ -20,6 +20,7 @@ import {
getDomainBase,
getDomainPath,
resolveAIMetrics,
resolveVectorStoreMetrics,
userInInstanceRanOutOfFreeAiCredits,
} from '@/telemetry-helpers';
import { randomInt } from '@/utils';
@@ -1808,3 +1809,221 @@ describe('makeAIMetrics', () => {
});
});
});
describe('resolveVectorStoreMetrics', () => {
const makeNode = (parameters: object, type: string) =>
({
parameters,
type,
typeVersion: 1,
id: '7cb0b373-715c-4a89-8bbb-3f238907bc86',
name: 'a name',
position: [0, 0],
}) as INode;
it('should return empty object if no vector store nodes are present', () => {
const nodes = [
makeNode(
{
mode: 'insert',
},
'n8n-nodes-base.nonVectorStoreNode',
),
];
const nodeTypes = mock<NodeTypes>({
getByNameAndVersion: () => ({
description: {
codex: {
categories: ['Non-AI'],
},
} as unknown as INodeTypeDescription,
}),
});
const run = mock<IRun>({
data: {
resultData: {
runData: {},
},
},
});
const result = resolveVectorStoreMetrics(nodes, nodeTypes, run);
expect(result).toMatchObject({});
});
it('should detect vector store nodes that inserted data', () => {
const nodes = [
makeNode(
{
mode: 'insert',
},
'n8n-nodes-base.vectorStoreNode',
),
];
const nodeTypes = mock<NodeTypes>({
getByNameAndVersion: () => ({
description: {
codex: {
categories: ['AI'],
subcategories: { AI: ['Vector Stores'] },
},
} as unknown as INodeTypeDescription,
}),
});
const run = mock<IRun>({
data: {
resultData: {
runData: {
'a name': [
{
executionStatus: 'success',
},
],
},
},
},
});
const result = resolveVectorStoreMetrics(nodes, nodeTypes, run);
expect(result).toMatchObject({
insertedIntoVectorStore: true,
queriedDataFromVectorStore: false,
});
});
it('should detect vector store nodes that queried data', () => {
const nodes = [
makeNode(
{
mode: 'retrieve',
},
'n8n-nodes-base.vectorStoreNode',
),
];
const nodeTypes = mock<NodeTypes>({
getByNameAndVersion: () => ({
description: {
codex: {
categories: ['AI'],
subcategories: { AI: ['Vector Stores'] },
},
} as unknown as INodeTypeDescription,
}),
});
const run = mock<IRun>({
data: {
resultData: {
runData: {
'a name': [
{
executionStatus: 'success',
},
],
},
},
},
});
const result = resolveVectorStoreMetrics(nodes, nodeTypes, run);
expect(result).toMatchObject({
insertedIntoVectorStore: false,
queriedDataFromVectorStore: true,
});
});
it('should detect vector store nodes that both inserted and queried data', () => {
const nodes = [
makeNode(
{
mode: 'insert',
},
'n8n-nodes-base.vectorStoreNode',
),
makeNode(
{
mode: 'retrieve',
},
'n8n-nodes-base.vectorStoreNode',
),
];
const nodeTypes = mock<NodeTypes>({
getByNameAndVersion: () => ({
description: {
codex: {
categories: ['AI'],
subcategories: { AI: ['Vector Stores'] },
},
} as unknown as INodeTypeDescription,
}),
});
const run = mock<IRun>({
data: {
resultData: {
runData: {
'a name': [
{
executionStatus: 'success',
},
],
},
},
},
});
const result = resolveVectorStoreMetrics(nodes, nodeTypes, run);
expect(result).toMatchObject({
insertedIntoVectorStore: true,
queriedDataFromVectorStore: true,
});
});
it('should return empty object if no successful executions are found', () => {
const nodes = [
makeNode(
{
mode: 'insert',
},
'n8n-nodes-base.vectorStoreNode',
),
];
const nodeTypes = mock<NodeTypes>({
getByNameAndVersion: () => ({
description: {
codex: {
categories: ['AI'],
subcategories: { AI: ['Vector Stores'] },
},
} as unknown as INodeTypeDescription,
}),
});
const run = mock<IRun>({
data: {
resultData: {
runData: {
'a name': [
{
executionStatus: 'error',
},
],
},
},
},
});
const result = resolveVectorStoreMetrics(nodes, nodeTypes, run);
expect(result).toMatchObject({
insertedIntoVectorStore: false,
queriedDataFromVectorStore: false,
});
});
});