Skip to content

Commit a632f40

Browse files
bergundyBashkirovNmjameswh
authored
Implement enhanced stack traces (#820)
Co-authored-by: Nik B <bashkirov.office@gmail.com> Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
1 parent 937b7f0 commit a632f40

File tree

15 files changed

+358
-56
lines changed

15 files changed

+358
-56
lines changed

package-lock.json

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/test/src/helpers.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,3 @@ export function cleanStackTrace(stack: string): string {
3232
const cleaned = su.clean(stack).trimEnd();
3333
return stack.split('\n')[0] + '\n' + (cleaned && cleaned.replace(/:\d+:\d+/g, '').replace(/^/gms, ' at '));
3434
}
35-
36-
export function containsMatching(strings: string[], regex: RegExp): boolean {
37-
console.log('strings:', strings);
38-
return strings.some((str) => regex.test(str));
39-
}

packages/test/src/integration-tests.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint @typescript-eslint/no-non-null-assertion: 0 */
2+
import path from 'node:path';
23
import {
34
ActivityFailure,
45
ApplicationFailure,
@@ -41,6 +42,7 @@ import { ConnectionInjectorInterceptor } from './activities/interceptors';
4142
import { cleanOptionalStackTrace, u8 } from './helpers';
4243
import * as workflows from './workflows';
4344
import { withZeroesHTTPServer } from './zeroes-http-server';
45+
import { readFileSync } from 'node:fs';
4446

4547
const { EVENT_TYPE_TIMER_STARTED, EVENT_TYPE_TIMER_FIRED, EVENT_TYPE_TIMER_CANCELED } =
4648
iface.temporal.api.enums.v1.EventType;
@@ -80,6 +82,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
8082
interceptors: {
8183
activityInbound: [() => new ConnectionInjectorInterceptor(connection, loadDataConverter(dataConverter))],
8284
},
85+
showStackTraceSources: true,
8386
});
8487

8588
const runPromise = worker.run();
@@ -402,7 +405,8 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
402405
await workflow.result();
403406
await t.throwsAsync(workflow.query('not found'), {
404407
instanceOf: QueryNotRegisteredError,
405-
message: 'Workflow did not register a handler for not found. Registered queries: [__stack_trace isBlocked]',
408+
message:
409+
'Workflow did not register a handler for not found. Registered queries: [__stack_trace __enhanced_stack_trace isBlocked]',
406410
});
407411
});
408412

@@ -1207,6 +1211,68 @@ export function runIntegrationTests(codec?: PayloadCodec): void {
12071211
at stackTracer (test/src/workflows/stack-tracer.ts)`
12081212
);
12091213
});
1214+
1215+
test('Enhanced stack trace returns trace that makes sense', async (t) => {
1216+
const { client } = t.context;
1217+
const workflowId = uuid4();
1218+
1219+
const enhancedStack = await client.execute(workflows.enhancedStackTracer, {
1220+
taskQueue: 'test',
1221+
workflowId,
1222+
});
1223+
1224+
const stacks = enhancedStack.stacks.map((s) => ({
1225+
locations: s.locations.map((l) => ({
1226+
...l,
1227+
...(l.filePath ? { filePath: l.filePath.replace(path.resolve(__dirname, '../../../'), '') } : undefined),
1228+
})),
1229+
}));
1230+
t.is(enhancedStack.sdk.name, 'typescript');
1231+
t.is(enhancedStack.sdk.version, pkg.version); // Expect workflow and worker versions to match
1232+
t.deepEqual(stacks, [
1233+
{
1234+
locations: [
1235+
{
1236+
functionName: 'Function.all',
1237+
},
1238+
{
1239+
filePath: '/packages/test/src/workflows/stack-tracer.ts',
1240+
functionName: 'enhancedStackTracer',
1241+
line: 32,
1242+
column: 35,
1243+
},
1244+
],
1245+
},
1246+
{
1247+
locations: [
1248+
{
1249+
filePath: '/packages/test/src/workflows/stack-tracer.ts',
1250+
functionName: 'enhancedStackTracer',
1251+
line: 32,
1252+
column: 35,
1253+
},
1254+
],
1255+
},
1256+
{
1257+
locations: [
1258+
{
1259+
functionName: 'Promise.then',
1260+
},
1261+
{
1262+
filePath: '/packages/workflow/src/trigger.ts',
1263+
functionName: 'Trigger.then',
1264+
line: 47,
1265+
column: 24,
1266+
},
1267+
],
1268+
},
1269+
]);
1270+
const expectedSources = ['../src/workflows/stack-tracer.ts', '../../workflow/src/trigger.ts'].map((p) => [
1271+
path.resolve(__dirname, p),
1272+
[{ content: readFileSync(path.resolve(__dirname, p), 'utf8'), lineOffset: 0 }],
1273+
]);
1274+
t.deepEqual(Object.entries(enhancedStack.sources), expectedSources);
1275+
});
12101276
}
12111277

12121278
test('issue-731', async (t) => {

packages/test/src/test-workflows.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ async function createWorkflow(
9797
randomnessSeed: Long.fromInt(1337).toBytes(),
9898
now: startTime,
9999
patches: [],
100+
showStackTraceSources: true,
100101
})) as VMWorkflow;
101102
return workflow;
102103
}

packages/test/src/workflows/stack-tracer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @module
44
*/
55
import * as wf from '@temporalio/workflow';
6+
import type { EnhancedStackTrace } from '@temporalio/workflow/src/interfaces';
67
import type * as activities from '../activities';
78
import { unblockOrCancel } from './unblock-or-cancel';
89

@@ -24,3 +25,17 @@ export async function stackTracer(): Promise<[string, string]> {
2425
const second = await queryOwnWf(wf.stackTraceQuery);
2526
return [first, second];
2627
}
28+
29+
export async function enhancedStackTracer(): Promise<EnhancedStackTrace> {
30+
const trigger = new wf.Trigger<EnhancedStackTrace>();
31+
32+
const [enhStack] = await Promise.all([
33+
trigger,
34+
Promise.race([
35+
queryOwnWf(wf.enhancedStackTraceQuery).then((stack) => trigger.resolve(stack)),
36+
wf.executeChild(unblockOrCancel),
37+
wf.sleep(100_000),
38+
]),
39+
]);
40+
return enhStack;
41+
}

packages/worker/src/worker-options.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ export interface WorkerOptions {
244244
*/
245245
enableSDKTracing?: boolean;
246246

247+
/**
248+
* Whether or not to send the sources in enhanced stack trace query responses
249+
*
250+
* @default false
251+
*/
252+
showStackTraceSources?: boolean;
253+
247254
/**
248255
* If `true` Worker runs Workflows in the same thread allowing debugger to
249256
* attach to Workflow instances.
@@ -292,6 +299,7 @@ export type WorkerOptionsWithDefaults = WorkerOptions &
292299
| 'maxHeartbeatThrottleInterval'
293300
| 'defaultHeartbeatThrottleInterval'
294301
| 'enableSDKTracing'
302+
| 'showStackTraceSources'
295303
| 'debugMode'
296304
>
297305
> & {
@@ -418,7 +426,7 @@ export function appendDefaultInterceptors(
418426
}
419427

420428
export function addDefaultWorkerOptions(options: WorkerOptions): WorkerOptionsWithDefaults {
421-
const { maxCachedWorkflows, debugMode, ...rest } = options;
429+
const { maxCachedWorkflows, showStackTraceSources, debugMode, ...rest } = options;
422430

423431
return {
424432
namespace: 'default',
@@ -437,6 +445,7 @@ export function addDefaultWorkerOptions(options: WorkerOptions): WorkerOptionsWi
437445
maxCachedWorkflows:
438446
maxCachedWorkflows ?? Math.floor(Math.max(v8.getHeapStatistics().heap_size_limit / GiB - 1, 1) * 250),
439447
enableSDKTracing: false,
448+
showStackTraceSources: showStackTraceSources ?? false,
440449
debugMode: debugMode ?? false,
441450
interceptors: appendDefaultInterceptors({}),
442451
sinks: defaultSinks(),

packages/worker/src/worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,7 @@ export class Worker {
11331133
randomnessSeed: randomnessSeed.toBytes(),
11341134
now: tsToMs(activation.timestamp),
11351135
patches,
1136+
showStackTraceSources: this.options.showStackTraceSources,
11361137
});
11371138
});
11381139

0 commit comments

Comments
 (0)