Skip to content

Commit 8e19f6a

Browse files
committed
Implement step sourcemaps
1 parent a5e98f8 commit 8e19f6a

File tree

20 files changed

+150
-29
lines changed

20 files changed

+150
-29
lines changed

.changeset/warm-flies-enjoy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@workflow/builders": patch
3+
"@workflow/core": patch
4+
---
5+
6+
Implement sourcemaps and trace propogation for steps

packages/builders/src/base-builder.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,11 @@ export abstract class BaseBuilder {
348348
keepNames: true,
349349
minify: false,
350350
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
351-
// TODO: investigate proper source map support
352-
sourcemap: EMIT_SOURCEMAPS_FOR_DEBUGGING,
351+
// Inline source maps for better stack traces in step execution.
352+
// Steps execute in Node.js context and inline sourcemaps ensure we get
353+
// meaningful stack traces with proper file names and line numbers when errors
354+
// occur in deeply nested function calls across multiple files.
355+
sourcemap: 'inline',
353356
plugins: [
354357
createSwcPlugin({
355358
mode: 'step',

packages/core/e2e/e2e.test.ts

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -571,29 +571,27 @@ describe('e2e', () => {
571571
expect(returnValue).toHaveProperty('cause');
572572
expect(returnValue.cause).toBeTypeOf('object');
573573
expect(returnValue.cause).toHaveProperty('message');
574-
expect(returnValue.cause.message).toContain(
575-
'Error from imported helper module'
576-
);
574+
expect(returnValue.cause.message).toContain('Error from workflow helper');
577575

578576
// Verify the stack trace is present in the cause
579577
expect(returnValue.cause).toHaveProperty('stack');
580578
expect(typeof returnValue.cause.stack).toBe('string');
581579

582580
// Known issue: SvelteKit dev mode has incorrect source map mappings for bundled imports.
583-
// esbuild with bundle:true inlines helpers.ts but source maps incorrectly map to 99_e2e.ts
581+
// esbuild with bundle:true inlines the helper but source maps incorrectly map to 99_e2e.ts
584582
// This works correctly in production and other frameworks.
585583
// TODO: Investigate esbuild source map generation for bundled modules
586584
const isSvelteKitDevMode =
587585
process.env.APP_NAME === 'sveltekit' && isLocalDeployment();
588586

589587
if (!isSvelteKitDevMode) {
590-
// Stack trace should include frames from the helper module (helpers.ts)
591-
expect(returnValue.cause.stack).toContain('helpers.ts');
588+
// Stack trace should include frames from the workflow error test module
589+
expect(returnValue.cause.stack).toContain('98_workflow_error_test.ts');
592590
}
593591

594592
// These checks should work in all modes
595-
expect(returnValue.cause.stack).toContain('throwError');
596-
expect(returnValue.cause.stack).toContain('callThrower');
593+
expect(returnValue.cause.stack).toContain('throwWorkflowError');
594+
expect(returnValue.cause.stack).toContain('workflowErrorHelper');
597595

598596
// Stack trace should include frames from the workflow file (99_e2e.ts)
599597
expect(returnValue.cause.stack).toContain('99_e2e.ts');
@@ -606,9 +604,83 @@ describe('e2e', () => {
606604
const { json: runData } = await cliInspectJson(`runs ${run.runId}`);
607605
expect(runData.status).toBe('failed');
608606
expect(runData.error).toBeTypeOf('object');
609-
expect(runData.error.message).toContain(
610-
'Error from imported helper module'
607+
expect(runData.error.message).toContain('Error from workflow helper');
608+
}
609+
);
610+
611+
test(
612+
'deepStepErrorWorkflow - stack traces work with step errors across multiple files',
613+
{ timeout: 60_000 },
614+
async () => {
615+
// This workflow intentionally throws a FatalError from a step that calls imported helpers
616+
// Call chain: deepStepErrorWorkflow -> deepStepWithNestedError (step) -> stepErrorHelper -> throwStepError
617+
// This verifies that stack traces preserve the call chain from step errors
618+
const run = await triggerWorkflow('deepStepErrorWorkflow', []);
619+
const returnValue = await getWorkflowReturnValue(run.runId);
620+
621+
// The workflow should fail with error response
622+
expect(returnValue).toHaveProperty('name');
623+
expect(returnValue.name).toBe('WorkflowRunFailedError');
624+
expect(returnValue).toHaveProperty('message');
625+
626+
// Verify the cause property contains the structured error
627+
expect(returnValue).toHaveProperty('cause');
628+
expect(returnValue.cause).toBeTypeOf('object');
629+
expect(returnValue.cause).toHaveProperty('message');
630+
expect(returnValue.cause.message).toContain('Error from step helper');
631+
632+
// Verify the stack trace contains the error chain
633+
expect(returnValue.cause).toHaveProperty('stack');
634+
expect(typeof returnValue.cause.stack).toBe('string');
635+
636+
// Log the full stack trace for debugging
637+
console.log('Full stack trace from deepStepErrorWorkflow:');
638+
console.log(returnValue.cause.stack);
639+
640+
// Known issue: SvelteKit dev mode has incorrect source map mappings for bundled imports.
641+
const isSvelteKitDevMode =
642+
process.env.APP_NAME === 'sveltekit' && isLocalDeployment();
643+
644+
if (!isSvelteKitDevMode) {
645+
// Stack trace should include frames from the step error test module
646+
expect(returnValue.cause.stack).toContain('98_step_error_test.ts');
647+
}
648+
649+
// These checks should work in all modes - verify the call chain
650+
// Bottom of stack: the error thrower
651+
expect(returnValue.cause.stack).toContain('throwStepError');
652+
653+
// Middle layer: helper function
654+
expect(returnValue.cause.stack).toContain('stepErrorHelper');
655+
656+
// Top layer: the step function
657+
expect(returnValue.cause.stack).toContain('deepStepWithNestedError');
658+
659+
// Note: Workflow functions don't appear in the step error's stack trace
660+
// because they execute in the workflow VM context, while the error
661+
// originates in the step execution Node.js context. This is expected.
662+
663+
// Stack trace should NOT contain 'evalmachine' anywhere
664+
expect(returnValue.cause.stack).not.toContain('evalmachine');
665+
666+
// Verify the run failed with structured error
667+
const { json: runData } = await cliInspectJson(`runs ${run.runId}`);
668+
expect(runData.status).toBe('failed');
669+
expect(runData.error).toBeTypeOf('object');
670+
expect(runData.error.message).toContain('Error from step helper');
671+
672+
// Verify it was a step execution failure (not a workflow execution failure)
673+
// The error should come from a step, so check the steps
674+
const { json: stepsData } = await cliInspectJson(
675+
`steps --runId ${run.runId}`
611676
);
677+
expect(Array.isArray(stepsData)).toBe(true);
678+
expect(stepsData.length).toBeGreaterThan(0);
679+
680+
// Find the failed step
681+
const failedStep = stepsData.find((s: any) => s.status === 'failed');
682+
expect(failedStep).toBeDefined();
683+
expect(failedStep.stepName).toContain('deepStepWithNestedError');
612684
}
613685
);
614686
});

packages/core/src/step.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ export function createUseStep(ctx: WorkflowOrchestratorContext) {
9191
// Step failed - bubble up to workflow
9292
if (event.eventData.fatal) {
9393
setTimeout(() => {
94-
reject(new FatalError(event.eventData.error));
94+
const error = new FatalError(event.eventData.error);
95+
// Preserve the original stack trace from the step execution
96+
// This ensures that deeply nested errors show the full call chain
97+
if (event.eventData.stack) {
98+
error.stack = event.eventData.stack;
99+
}
100+
reject(error);
95101
}, 0);
96102
return EventConsumerResult.Finished;
97103
} else {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Step error test helpers - functions that execute in the step (Node.js) context
2+
// These demonstrate stack trace preservation for errors thrown in step execution
3+
4+
import { FatalError } from 'workflow';
5+
6+
export function throwStepError() {
7+
throw new FatalError('Error from step helper');
8+
}
9+
10+
export function stepErrorHelper() {
11+
throwStepError();
12+
}
13+
14+
export async function deepStepWithNestedError() {
15+
'use step';
16+
stepErrorHelper();
17+
return 'never reached';
18+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Workflow error test helpers - functions that execute in the workflow VM context
2+
// These demonstrate stack trace preservation for errors thrown in workflow execution
3+
4+
export function throwWorkflowError() {
5+
throw new Error('Error from workflow helper');
6+
}
7+
8+
export function workflowErrorHelper() {
9+
throwWorkflowError();
10+
}

workbench/example/workflows/99_e2e.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
RetryableError,
1111
sleep,
1212
} from 'workflow';
13-
import { callThrower } from './helpers.js';
13+
import { workflowErrorHelper } from './98_workflow_error_test.js';
14+
import { deepStepWithNestedError } from './98_step_error_test.js';
1415

1516
//////////////////////////////////////////////////////////
1617

@@ -443,8 +444,18 @@ async function stepThatThrowsRetryableError() {
443444

444445
export async function crossFileErrorWorkflow() {
445446
'use workflow';
446-
// This will throw an error from the imported helpers.ts file
447-
callThrower();
447+
// This will throw an error from the imported 98_workflow_error_test.ts file
448+
workflowErrorHelper();
449+
return 'never reached';
450+
}
451+
452+
//////////////////////////////////////////////////////////
453+
454+
export async function deepStepErrorWorkflow() {
455+
'use workflow';
456+
// This workflow calls a step that throws an error through a helper chain
457+
// Call chain: deepStepErrorWorkflow -> deepStepWithNestedError (step) -> stepErrorHelper -> throwStepError
458+
await deepStepWithNestedError();
448459
return 'never reached';
449460
}
450461

workbench/example/workflows/helpers.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../example/workflows/98_step_error_test.ts
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../example/workflows/98_workflow_error_test.ts

0 commit comments

Comments
 (0)