Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/durabletask-js/src/tracing/trace-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,9 +690,17 @@ export function setOrchestrationStatusFromActions(
span.setAttribute(DurableTaskAttributes.TASK_STATUS, statusName);
}

// Match .NET: set span error status when orchestration completes with Failed
// Match .NET: set span error status when orchestration completes with Failed.
// Read error message from failureDetails (where setFailed() stores it),
// falling back to result for backwards compatibility. Use nullish coalescing
// (like the task/sub-orchestration failure paths above and .NET's TraceHelper)
// so an explicitly empty failureDetails message is preserved rather than
// masked by the generic fallback.
if (status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED) {
const errorMessage = completeAction.getResult()?.getValue() ?? "Orchestration failed";
const errorMessage =
completeAction.getFailuredetails()?.getErrormessage() ??
completeAction.getResult()?.getValue() ??
"Orchestration failed";
Comment thread
YunchuWang marked this conversation as resolved.
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: errorMessage });
} else {
span.setStatus({ code: otel.SpanStatusCode.OK });
Expand Down
78 changes: 74 additions & 4 deletions packages/durabletask-js/test/tracing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1124,15 +1124,17 @@ describe("Trace Helper - setOrchestrationStatusFromActions", () => {
expect(spans[0].status.code).toBe(otel.SpanStatusCode.OK);
});

it("should set status attribute for failed orchestration and ERROR span status", () => {
it("should set status attribute for failed orchestration and ERROR span status with failureDetails", () => {
const tracer = otel.trace.getTracer(TRACER_NAME);
const span = tracer.startSpan("orch-status-failed");

// Match the real runtime path: setFailed() sets failureDetails but NOT result
const completeAction = new pb.CompleteOrchestrationAction();
completeAction.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
const resultValue = new StringValue();
resultValue.setValue("User code threw an error");
completeAction.setResult(resultValue);
const failureDetails = new pb.TaskFailureDetails();
failureDetails.setErrormessage("User code threw an error");
failureDetails.setErrortype("Error");
completeAction.setFailuredetails(failureDetails);

const action = new pb.OrchestratorAction();
action.setCompleteorchestration(completeAction);
Expand All @@ -1146,6 +1148,74 @@ describe("Trace Helper - setOrchestrationStatusFromActions", () => {
expect(spans[0].status.message).toBe("User code threw an error");
});

it("should fall back to result field for failed orchestration error message", () => {
const tracer = otel.trace.getTracer(TRACER_NAME);
const span = tracer.startSpan("orch-status-failed-result-fallback");

// Edge case: no failureDetails, but result is set (backwards compatibility)
const completeAction = new pb.CompleteOrchestrationAction();
completeAction.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
const resultValue = new StringValue();
resultValue.setValue("Legacy error message");
completeAction.setResult(resultValue);

const action = new pb.OrchestratorAction();
action.setCompleteorchestration(completeAction);

setOrchestrationStatusFromActions(span, [action]);
span.end();

const spans = exporter.getFinishedSpans();
expect(spans[0].status.code).toBe(otel.SpanStatusCode.ERROR);
expect(spans[0].status.message).toBe("Legacy error message");
});

it("should use default error message when neither failureDetails nor result is set", () => {
const tracer = otel.trace.getTracer(TRACER_NAME);
const span = tracer.startSpan("orch-status-failed-no-details");

const completeAction = new pb.CompleteOrchestrationAction();
completeAction.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);

const action = new pb.OrchestratorAction();
action.setCompleteorchestration(completeAction);

setOrchestrationStatusFromActions(span, [action]);
span.end();

const spans = exporter.getFinishedSpans();
expect(spans[0].status.code).toBe(otel.SpanStatusCode.ERROR);
expect(spans[0].status.message).toBe("Orchestration failed");
});

it("should preserve an explicitly empty failureDetails message over the result/default fallback", () => {
const tracer = otel.trace.getTracer(TRACER_NAME);
const span = tracer.startSpan("orch-status-failed-empty-message");

// failureDetails is present (orchestration failed) but the error message is
// empty. failureDetails is authoritative, so nullish coalescing must keep the
// empty string rather than falling through to result/"Orchestration failed".
const completeAction = new pb.CompleteOrchestrationAction();
completeAction.setOrchestrationstatus(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
const failureDetails = new pb.TaskFailureDetails();
failureDetails.setErrormessage("");
failureDetails.setErrortype("Error");
completeAction.setFailuredetails(failureDetails);
const resultValue = new StringValue();
resultValue.setValue("Should not be used");
completeAction.setResult(resultValue);

const action = new pb.OrchestratorAction();
action.setCompleteorchestration(completeAction);

setOrchestrationStatusFromActions(span, [action]);
span.end();

const spans = exporter.getFinishedSpans();
expect(spans[0].status.code).toBe(otel.SpanStatusCode.ERROR);
expect(spans[0].status.message).toBe("");
});

it("should not set status when no completion action present", () => {
const tracer = otel.trace.getTracer(TRACER_NAME);
const span = tracer.startSpan("orch-status-none");
Expand Down
Loading