mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(API): Return an execution when its stored trace context is incomplete (#39715)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
918bcddb4f
commit
dfd7a52224
|
|
@ -30,6 +30,7 @@ import type {
|
|||
PolicyAttachment,
|
||||
PolicyRule,
|
||||
} from '@/modules/type-availability-policies/policy-rule.types';
|
||||
import type { TracingContext } from '@/modules/otel/tracing-context';
|
||||
import type { McpCallerAuth } from '@/services/oauth-token-verifier-proxy.service';
|
||||
|
||||
import type { AiEventMap } from './ai.event-map';
|
||||
|
|
@ -658,7 +659,7 @@ export type RelayEventMap = {
|
|||
stoppedAt: Date;
|
||||
detector: CrashDetector;
|
||||
hostId: string;
|
||||
tracingContext?: { traceparent: string; tracestate?: string };
|
||||
tracingContext?: TracingContext;
|
||||
workflowVersionId?: string;
|
||||
retryOf?: string;
|
||||
workflowCustomTelemetryTags?: IWorkflowSettings['customTelemetryTags'];
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { TestNodeWithTracing } from './test-node-with-tracing';
|
|||
import { OtelSettingsService } from '../../otel-settings.service';
|
||||
import { OtelConfig } from '../../otel.config';
|
||||
import { OtelService } from '../../otel.service';
|
||||
import type { TracingContext } from '../../tracing-context';
|
||||
|
||||
const BASE_DIR = path.resolve(__dirname, '../../../../../..');
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ export async function executeWorkflow(
|
|||
options: {
|
||||
mode?: 'webhook' | 'trigger' | 'manual' | 'retry';
|
||||
retryOf?: string;
|
||||
tracingContext?: { traceparent: string; tracestate?: string };
|
||||
tracingContext?: TracingContext;
|
||||
triggerData?: IDataObject;
|
||||
} = {},
|
||||
): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
Put,
|
||||
Query,
|
||||
} from '@n8n/decorators';
|
||||
import { isRecord } from '@n8n/utils/is-record';
|
||||
import type { Response } from 'express';
|
||||
import { replaceCircularReferences, WorkflowOperationError } from 'n8n-workflow';
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ import { isRedactableExecution } from '@/executions/execution-redaction';
|
|||
import { ExecutionRedactionServiceProxy } from '@/executions/execution-redaction-proxy.service';
|
||||
import { ExecutionService } from '@/executions/execution.service';
|
||||
import type { StopResult } from '@/executions/execution.types';
|
||||
import type { TracingContext } from '@/modules/otel/tracing-context';
|
||||
import { decodeCursor, encodeNextCursor } from '@/public-api/v1/shared/services/pagination.service';
|
||||
import { WorkflowSharingService } from '@/workflows/workflow-sharing.service';
|
||||
|
||||
|
|
@ -434,6 +436,15 @@ export class ExecutionsPublicController {
|
|||
}
|
||||
}
|
||||
|
||||
function toPublicTracingContext(tracingContext: unknown): TracingContext | null {
|
||||
if (!isRecord(tracingContext)) return null;
|
||||
|
||||
const { traceparent, tracestate } = tracingContext;
|
||||
if (typeof traceparent !== 'string' || traceparent.length === 0) return null;
|
||||
|
||||
return typeof tracestate === 'string' ? { traceparent, tracestate } : { traceparent };
|
||||
}
|
||||
|
||||
function toBaseFields(execution: PublicExecution) {
|
||||
return {
|
||||
finished: execution.finished,
|
||||
|
|
@ -448,7 +459,7 @@ function toBaseFields(execution: PublicExecution) {
|
|||
workflowId: execution.workflowId,
|
||||
waitTill: execution.waitTill ? execution.waitTill.toISOString() : null,
|
||||
storedAt: execution.storedAt,
|
||||
tracingContext: execution.tracingContext ?? null,
|
||||
tracingContext: toPublicTracingContext(execution.tracingContext),
|
||||
deduplicationKey: execution.deduplicationKey ?? null,
|
||||
jsonSizeBytes: execution.jsonSizeBytes ?? 0,
|
||||
binaryDataSizeBytes: execution.binaryDataSizeBytes ?? 0,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { stringify } from 'flatted';
|
|||
import type { ExecutionStatus, IRunExecutionData, IRunExecutionDataAll } from 'n8n-workflow';
|
||||
import { WAIT_FOR_SUB_EXECUTION, WAIT_INDEFINITELY } from 'n8n-workflow';
|
||||
|
||||
import type { TracingContext } from '@/modules/otel/tracing-context';
|
||||
|
||||
describe('ExecutionRepository', () => {
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
|
@ -192,7 +194,7 @@ describe('ExecutionRepository', () => {
|
|||
status: ExecutionStatus,
|
||||
extra: {
|
||||
waitTill?: Date;
|
||||
tracingContext?: { traceparent: string };
|
||||
tracingContext?: TracingContext;
|
||||
workflowVersionId?: string;
|
||||
retryOf?: string;
|
||||
} = {},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { randomBytes } from 'node:crypto';
|
||||
import {
|
||||
createManyWorkflows,
|
||||
createTeamProject,
|
||||
|
|
@ -93,6 +94,12 @@ const testWithAPIKey =
|
|||
expect(response.statusCode).toBe(401);
|
||||
};
|
||||
|
||||
function createTraceparent() {
|
||||
const traceId = randomBytes(16).toString('hex');
|
||||
const spanId = randomBytes(8).toString('hex');
|
||||
return `00-${traceId}-${spanId}-01`;
|
||||
}
|
||||
|
||||
describe('GET /executions/:id', () => {
|
||||
test('should fail due to missing API Key', testWithAPIKey('get', '/executions/1', null));
|
||||
|
||||
|
|
@ -110,8 +117,16 @@ describe('GET /executions/:id', () => {
|
|||
|
||||
test('owner should be able to get an execution owned by him', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const traceparent = createTraceparent();
|
||||
|
||||
const execution = await createSuccessfulExecution(workflow);
|
||||
const execution = await createExecution(
|
||||
{
|
||||
finished: true,
|
||||
status: 'success',
|
||||
tracingContext: { traceparent },
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
|
||||
const response = await authOwnerAgent.get(`/executions/${execution.id}`);
|
||||
|
||||
|
|
@ -127,6 +142,7 @@ describe('GET /executions/:id', () => {
|
|||
stoppedAt,
|
||||
workflowId,
|
||||
waitTill,
|
||||
tracingContext,
|
||||
} = response.body;
|
||||
|
||||
expect(id).toBeDefined();
|
||||
|
|
@ -138,6 +154,53 @@ describe('GET /executions/:id', () => {
|
|||
expect(stoppedAt).not.toBeNull();
|
||||
expect(workflowId).toBe(execution.workflowId);
|
||||
expect(waitTill).toBeNull();
|
||||
expect(tracingContext).toEqual({ traceparent });
|
||||
});
|
||||
|
||||
test('should return a webhook execution when the stored tracestate is null', async () => {
|
||||
const traceparent = createTraceparent();
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const execution = await createExecution(
|
||||
{
|
||||
finished: true,
|
||||
status: 'success',
|
||||
mode: 'webhook',
|
||||
tracingContext: {
|
||||
traceparent,
|
||||
tracestate: null,
|
||||
} as unknown as ExecutionEntity['tracingContext'],
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
|
||||
const withoutData = await authOwnerAgent.get(`/executions/${execution.id}`);
|
||||
const withData = await authOwnerAgent.get(`/executions/${execution.id}?includeData=true`);
|
||||
|
||||
expect(withoutData.statusCode).toBe(200);
|
||||
expect(withData.statusCode).toBe(200);
|
||||
expect(withoutData.body.tracingContext).toEqual({ traceparent });
|
||||
expect(withData.body.tracingContext).toEqual({ traceparent });
|
||||
});
|
||||
|
||||
test('should return a webhook execution when the stored traceparent is missing', async () => {
|
||||
const workflow = await createWorkflow({}, owner);
|
||||
const execution = await createExecution(
|
||||
{
|
||||
finished: true,
|
||||
status: 'success',
|
||||
mode: 'webhook',
|
||||
tracingContext: {} as unknown as ExecutionEntity['tracingContext'],
|
||||
},
|
||||
workflow,
|
||||
);
|
||||
|
||||
const withoutData = await authOwnerAgent.get(`/executions/${execution.id}`);
|
||||
const withData = await authOwnerAgent.get(`/executions/${execution.id}?includeData=true`);
|
||||
|
||||
expect(withoutData.statusCode).toBe(200);
|
||||
expect(withData.statusCode).toBe(200);
|
||||
expect(withoutData.body.tracingContext).toBeNull();
|
||||
expect(withData.body.tracingContext).toBeNull();
|
||||
});
|
||||
|
||||
test('owner should be able to read executions of other users', async () => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export async function createExecution(
|
|||
jsonSizeBytes,
|
||||
binaryDataSizeBytes,
|
||||
workflowVersionId,
|
||||
tracingContext,
|
||||
} = attributes;
|
||||
|
||||
const execution = await Container.get(ExecutionRepository).save({
|
||||
|
|
@ -62,6 +63,7 @@ export async function createExecution(
|
|||
...(jsonSizeBytes !== undefined && { jsonSizeBytes }),
|
||||
...(binaryDataSizeBytes !== undefined && { binaryDataSizeBytes }),
|
||||
...(workflowVersionId !== undefined && { workflowVersionId }),
|
||||
...(tracingContext !== undefined && { tracingContext }),
|
||||
});
|
||||
|
||||
if (metadata?.length) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user