@outputai/core 0.6.0 → 0.6.1-dev.aab2335.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/activity_integration/context.d.ts +5 -9
- package/src/activity_integration/context.js +5 -4
- package/src/activity_integration/context.spec.js +10 -15
- package/src/activity_integration/events.d.ts +2 -4
- package/src/activity_integration/events.js +8 -3
- package/src/activity_integration/events.spec.js +58 -29
- package/src/bus.js +18 -9
- package/src/bus.spec.js +30 -0
- package/src/hooks/index.d.ts +112 -58
- package/src/hooks/index.js +15 -12
- package/src/hooks/index.spec.js +60 -32
- package/src/interface/workflow.js +19 -35
- package/src/interface/workflow.spec.js +104 -15
- package/src/internal_activities/index.js +3 -3
- package/src/internal_activities/index.spec.js +31 -1
- package/src/internal_utils/temporal_context.js +12 -0
- package/src/internal_utils/temporal_context.spec.ts +83 -0
- package/src/internal_utils/trace_info.js +21 -0
- package/src/internal_utils/trace_info.spec.js +47 -0
- package/src/internal_utils/workflow_context.js +29 -0
- package/src/internal_utils/workflow_context.spec.js +46 -0
- package/src/logger/development.js +61 -0
- package/src/logger/development.spec.js +70 -0
- package/src/logger/index.js +14 -0
- package/src/logger/index.spec.js +27 -0
- package/src/logger/production.js +15 -0
- package/src/logger/production.spec.js +52 -0
- package/src/tracing/internal_interface.js +4 -4
- package/src/tracing/processors/local/index.js +21 -26
- package/src/tracing/processors/local/index.spec.js +39 -45
- package/src/tracing/processors/s3/index.js +13 -23
- package/src/tracing/processors/s3/index.spec.js +33 -26
- package/src/tracing/trace_attribute.js +0 -1
- package/src/tracing/trace_engine.js +8 -12
- package/src/tracing/trace_engine.spec.js +31 -27
- package/src/worker/configs.js +2 -0
- package/src/worker/configs.spec.js +25 -0
- package/src/worker/index.js +4 -1
- package/src/worker/index.spec.js +4 -0
- package/src/worker/interceptors/activity.js +31 -29
- package/src/worker/interceptors/activity.spec.js +58 -26
- package/src/worker/interceptors/workflow.js +7 -2
- package/src/worker/interceptors/workflow.spec.js +42 -6
- package/src/worker/log_hooks.js +35 -46
- package/src/worker/log_hooks.spec.js +43 -46
- package/src/worker/setup_telemetry.js +19 -0
- package/src/worker/setup_telemetry.spec.js +80 -0
- package/src/worker/sinks.js +24 -24
- package/src/interface/workflow_context.js +0 -33
- package/src/logger.js +0 -73
package/src/hooks/index.spec.js
CHANGED
|
@@ -25,6 +25,30 @@ import {
|
|
|
25
25
|
onWorkflowStart
|
|
26
26
|
} from './index.js';
|
|
27
27
|
|
|
28
|
+
const workflowDetails = {
|
|
29
|
+
workflowId: 'wf-1',
|
|
30
|
+
runId: 'run-1',
|
|
31
|
+
workflowType: 'myWorkflow',
|
|
32
|
+
firstExecutionRunId: 'run-1',
|
|
33
|
+
startTime: 1710000000000,
|
|
34
|
+
runStartTime: 1710000000000,
|
|
35
|
+
attempt: 1
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const catalogWorkflowDetails = {
|
|
39
|
+
...workflowDetails,
|
|
40
|
+
workflowType: WORKFLOW_CATALOG
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const activityInfo = {
|
|
44
|
+
activityId: 'act-1',
|
|
45
|
+
activityType: 'wf#step',
|
|
46
|
+
workflowExecution: { workflowId: 'wf-1', runId: 'run-1' },
|
|
47
|
+
workflowType: 'myWorkflow'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const eventDate = 1710000001234;
|
|
51
|
+
|
|
28
52
|
describe( 'hooks/index', () => {
|
|
29
53
|
beforeEach( () => {
|
|
30
54
|
vi.clearAllMocks();
|
|
@@ -43,49 +67,53 @@ describe( 'hooks/index', () => {
|
|
|
43
67
|
expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
|
|
44
68
|
} );
|
|
45
69
|
|
|
46
|
-
it( 'invokes handler with activity-shaped payload, forwarding
|
|
70
|
+
it( 'invokes handler with activity-shaped payload, forwarding bus fields', async () => {
|
|
47
71
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
48
72
|
onError( handler );
|
|
49
73
|
|
|
50
74
|
const err = new Error( 'act-fail' );
|
|
51
75
|
await onHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
52
76
|
eventId: 'evt-act-1',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
error: err
|
|
77
|
+
eventDate,
|
|
78
|
+
activityInfo,
|
|
79
|
+
workflowDetails,
|
|
80
|
+
outputActivityKind: 'step',
|
|
81
|
+
error: err,
|
|
82
|
+
extra: 'passthrough'
|
|
58
83
|
} );
|
|
59
84
|
|
|
60
85
|
expect( handler ).toHaveBeenCalledWith( {
|
|
61
86
|
eventId: 'evt-act-1',
|
|
87
|
+
eventDate,
|
|
62
88
|
source: 'activity',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
activityInfo,
|
|
90
|
+
workflowDetails,
|
|
91
|
+
outputActivityKind: 'step',
|
|
92
|
+
error: err,
|
|
93
|
+
extra: 'passthrough'
|
|
68
94
|
} );
|
|
69
95
|
} );
|
|
70
96
|
|
|
71
|
-
it( 'invokes handler with workflow-shaped payload, forwarding
|
|
97
|
+
it( 'invokes handler with workflow-shaped payload, forwarding bus fields', async () => {
|
|
72
98
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
73
99
|
onError( handler );
|
|
74
100
|
|
|
75
101
|
const err = new Error( 'wf-fail' );
|
|
76
102
|
await onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
77
103
|
eventId: 'evt-wf-1',
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
error: err
|
|
104
|
+
eventDate,
|
|
105
|
+
workflowDetails,
|
|
106
|
+
error: err,
|
|
107
|
+
extra: 'passthrough'
|
|
81
108
|
} );
|
|
82
109
|
|
|
83
110
|
expect( handler ).toHaveBeenCalledWith( {
|
|
84
111
|
eventId: 'evt-wf-1',
|
|
112
|
+
eventDate,
|
|
85
113
|
source: 'workflow',
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
114
|
+
workflowDetails,
|
|
115
|
+
error: err,
|
|
116
|
+
extra: 'passthrough'
|
|
89
117
|
} );
|
|
90
118
|
} );
|
|
91
119
|
|
|
@@ -94,9 +122,9 @@ describe( 'hooks/index', () => {
|
|
|
94
122
|
onError( handler );
|
|
95
123
|
|
|
96
124
|
const error = new Error( 'rt' );
|
|
97
|
-
await onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', error } );
|
|
125
|
+
await onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
|
|
98
126
|
|
|
99
|
-
expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', source: 'runtime', error } );
|
|
127
|
+
expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', eventDate, source: 'runtime', error } );
|
|
100
128
|
} );
|
|
101
129
|
} );
|
|
102
130
|
|
|
@@ -113,59 +141,59 @@ describe( 'hooks/index', () => {
|
|
|
113
141
|
} );
|
|
114
142
|
|
|
115
143
|
describe( 'onWorkflowStart', () => {
|
|
116
|
-
it( 'skips catalog workflow and forwards
|
|
144
|
+
it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
|
|
117
145
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
118
146
|
onWorkflowStart( handler );
|
|
119
147
|
|
|
120
148
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
121
|
-
eventId: 'evt-ignored',
|
|
149
|
+
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
122
150
|
} ) );
|
|
123
151
|
expect( handler ).not.toHaveBeenCalled();
|
|
124
152
|
|
|
125
153
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
126
|
-
eventId: 'evt-start-1',
|
|
154
|
+
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
127
155
|
} ) );
|
|
128
156
|
expect( handler ).toHaveBeenCalledWith( {
|
|
129
|
-
eventId: 'evt-start-1',
|
|
157
|
+
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
130
158
|
} );
|
|
131
159
|
} );
|
|
132
160
|
} );
|
|
133
161
|
|
|
134
162
|
describe( 'onWorkflowEnd', () => {
|
|
135
|
-
it( 'skips catalog workflow and forwards
|
|
163
|
+
it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
|
|
136
164
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
137
165
|
onWorkflowEnd( handler );
|
|
138
166
|
|
|
139
167
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
|
|
140
|
-
eventId: 'evt-ignored',
|
|
168
|
+
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
141
169
|
} ) );
|
|
142
170
|
expect( handler ).not.toHaveBeenCalled();
|
|
143
171
|
|
|
144
172
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
|
|
145
|
-
eventId: 'evt-end-1',
|
|
173
|
+
eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
146
174
|
} ) );
|
|
147
175
|
expect( handler ).toHaveBeenCalledWith( {
|
|
148
|
-
eventId: 'evt-end-1',
|
|
176
|
+
eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
149
177
|
} );
|
|
150
178
|
} );
|
|
151
179
|
} );
|
|
152
180
|
|
|
153
181
|
describe( 'onWorkflowError', () => {
|
|
154
|
-
it( 'skips catalog workflow and forwards
|
|
182
|
+
it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
|
|
155
183
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
156
184
|
const err = new Error( 'wf' );
|
|
157
185
|
onWorkflowError( handler );
|
|
158
186
|
|
|
159
187
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
160
|
-
eventId: 'evt-ignored',
|
|
188
|
+
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails, error: err
|
|
161
189
|
} ) );
|
|
162
190
|
expect( handler ).not.toHaveBeenCalled();
|
|
163
191
|
|
|
164
192
|
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
165
|
-
eventId: 'evt-err-1',
|
|
193
|
+
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
166
194
|
} ) );
|
|
167
195
|
expect( handler ).toHaveBeenCalledWith( {
|
|
168
|
-
eventId: 'evt-err-1',
|
|
196
|
+
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
169
197
|
} );
|
|
170
198
|
} );
|
|
171
199
|
} );
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
|
-
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy
|
|
2
|
+
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy } from '@temporalio/workflow';
|
|
3
3
|
import { defineSignal, setHandler } from '@temporalio/workflow';
|
|
4
4
|
import { validateWorkflow } from './validations/static.js';
|
|
5
5
|
import { validateWithSchema } from './validations/runtime.js';
|
|
6
|
+
import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
7
|
+
import { FatalError, ValidationError } from '#errors';
|
|
8
|
+
import { WorkflowContext } from '#internal_utils/workflow_context';
|
|
9
|
+
import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
|
|
10
|
+
import { extractErrorDetail } from '#internal_utils/errors';
|
|
11
|
+
import { TraceInfo } from '#internal_utils/trace_info';
|
|
6
12
|
import {
|
|
7
13
|
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
8
14
|
ACTIVITY_WRAPPER_VERSION_FIELD,
|
|
@@ -11,11 +17,6 @@ import {
|
|
|
11
17
|
Signal,
|
|
12
18
|
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
13
19
|
} from '#consts';
|
|
14
|
-
import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
15
|
-
import { FatalError, ValidationError } from '#errors';
|
|
16
|
-
import { Context } from './workflow_context.js';
|
|
17
|
-
import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
|
|
18
|
-
import { extractErrorDetail } from '#internal_utils/errors';
|
|
19
20
|
|
|
20
21
|
const defaultOptions = {
|
|
21
22
|
activityOptions: {
|
|
@@ -54,37 +55,21 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
54
55
|
// this returns a plain function, for example, in unit tests
|
|
55
56
|
if ( !inWorkflowContext() ) {
|
|
56
57
|
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
57
|
-
const
|
|
58
|
-
workflowId: 'test-workflow',
|
|
59
|
-
runId: 'test-run',
|
|
60
|
-
continueAsNew: async () => {},
|
|
61
|
-
isContinueAsNewSuggested: () => false
|
|
62
|
-
} );
|
|
63
|
-
const output = await fn( input, deepMerge( context, extra.context ) );
|
|
58
|
+
const output = await fn( input, deepMerge( WorkflowContext.build(), extra.context ) );
|
|
64
59
|
validateWithSchema( outputSchema, output, `Workflow ${name} output` );
|
|
65
60
|
return output;
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
const { workflowId,
|
|
69
|
-
const context =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const isRoot = !memo.executionContext;
|
|
73
|
-
|
|
74
|
-
/* Creates the execution context object or preserve if it already exists:
|
|
75
|
-
It will always contain the information about the root workflow
|
|
76
|
-
It will be used to as context for tracing (connecting events) */
|
|
77
|
-
const executionContext = memo.executionContext ?? {
|
|
78
|
-
workflowId,
|
|
79
|
-
runId,
|
|
80
|
-
workflowName: name,
|
|
81
|
-
disableTrace,
|
|
82
|
-
startTime: startTime.getTime()
|
|
83
|
-
};
|
|
63
|
+
const { workflowId, memo, root } = workflowInfo();
|
|
64
|
+
const context = WorkflowContext.build();
|
|
65
|
+
|
|
66
|
+
const isRoot = !root;
|
|
84
67
|
|
|
68
|
+
// Creates the immutable memo that will be used by all nested workflows/activities
|
|
69
|
+
// Preserves all info that already exists in the memo object, so new child workflows inherit this
|
|
85
70
|
Object.assign( memo, {
|
|
86
|
-
|
|
87
|
-
activityOptions: memo.activityOptions ?? activityOptions
|
|
71
|
+
traceInfo: memo.traceInfo ?? TraceInfo.build( { disableTrace } ),
|
|
72
|
+
activityOptions: memo.activityOptions ?? activityOptions
|
|
88
73
|
} );
|
|
89
74
|
|
|
90
75
|
/**
|
|
@@ -94,7 +79,7 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
94
79
|
* @TODO [OUT-468]
|
|
95
80
|
*/
|
|
96
81
|
const getTraceDestinations = async () => {
|
|
97
|
-
const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS](
|
|
82
|
+
const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo );
|
|
98
83
|
return isActivityResultWrapped( result ) ? result.output : result;
|
|
99
84
|
};
|
|
100
85
|
|
|
@@ -166,12 +151,11 @@ export function workflow( { name, description, inputSchema, outputSchema, fn, op
|
|
|
166
151
|
startWorkflow: async ( childName, input, extra = {} ) => {
|
|
167
152
|
try {
|
|
168
153
|
const result = await executeChild( childName, {
|
|
169
|
-
args: input ? [
|
|
154
|
+
args: undefined === input ? [] : [ input ],
|
|
170
155
|
workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
|
|
171
156
|
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
172
157
|
memo: {
|
|
173
|
-
|
|
174
|
-
parentId: workflowId,
|
|
158
|
+
...memo,
|
|
175
159
|
...( extra?.options?.activityOptions && { activityOptions: deepMerge( activityOptions, extra.options.activityOptions ) } )
|
|
176
160
|
}
|
|
177
161
|
} );
|
|
@@ -5,6 +5,8 @@ import { z } from 'zod';
|
|
|
5
5
|
const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
|
|
6
6
|
const defineSignalMock = vi.hoisted( () => vi.fn( name => name ) );
|
|
7
7
|
const setHandlerMock = vi.hoisted( () => vi.fn() );
|
|
8
|
+
const workflowContextBuildMock = vi.hoisted( () => vi.fn() );
|
|
9
|
+
const traceInfoBuildMock = vi.hoisted( () => vi.fn() );
|
|
8
10
|
const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
9
11
|
const executeChildMock = vi.fn().mockResolvedValue( undefined );
|
|
10
12
|
const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
|
|
@@ -31,6 +33,7 @@ const proxyActivitiesMock = vi.fn( () => {
|
|
|
31
33
|
const workflowInfoReturn = {
|
|
32
34
|
workflowId: 'wf-test-123',
|
|
33
35
|
workflowType: 'test_wf',
|
|
36
|
+
runId: 'run-test-123',
|
|
34
37
|
memo: {},
|
|
35
38
|
startTime: new Date( '2025-01-01T00:00:00Z' ),
|
|
36
39
|
continueAsNewSuggested: false
|
|
@@ -56,6 +59,14 @@ vi.mock( '@temporalio/workflow', () => ( {
|
|
|
56
59
|
setHandler: ( ...args ) => setHandlerMock( ...args )
|
|
57
60
|
} ) );
|
|
58
61
|
|
|
62
|
+
vi.mock( '#internal_utils/workflow_context', () => ( {
|
|
63
|
+
WorkflowContext: { build: workflowContextBuildMock }
|
|
64
|
+
} ) );
|
|
65
|
+
|
|
66
|
+
vi.mock( '#internal_utils/trace_info', () => ( {
|
|
67
|
+
TraceInfo: { build: traceInfoBuildMock }
|
|
68
|
+
} ) );
|
|
69
|
+
|
|
59
70
|
vi.mock( '#consts', async importOriginal => {
|
|
60
71
|
const actual = await importOriginal();
|
|
61
72
|
return {
|
|
@@ -70,8 +81,20 @@ describe( 'workflow()', () => {
|
|
|
70
81
|
vi.clearAllMocks();
|
|
71
82
|
inWorkflowContextMock.mockReturnValue( true );
|
|
72
83
|
defineSignalMock.mockImplementation( name => name );
|
|
73
|
-
workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
|
|
74
84
|
workflowInfoReturn.memo = {};
|
|
85
|
+
delete workflowInfoReturn.root;
|
|
86
|
+
workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
|
|
87
|
+
workflowContextBuildMock.mockReturnValue( {
|
|
88
|
+
control: {},
|
|
89
|
+
info: { workflowId: 'test-workflow', runId: 'test-run' }
|
|
90
|
+
} );
|
|
91
|
+
traceInfoBuildMock.mockImplementation( ( { disableTrace } ) => ( {
|
|
92
|
+
workflowId: 'trace-workflow-id',
|
|
93
|
+
workflowType: 'trace-workflow-type',
|
|
94
|
+
runId: 'trace-run-id',
|
|
95
|
+
startTime: 12345,
|
|
96
|
+
disableTrace
|
|
97
|
+
} ) );
|
|
75
98
|
proxyActivitiesMock.mockImplementation( () => {
|
|
76
99
|
stepSpyRef.current = vi.fn().mockResolvedValue( {} );
|
|
77
100
|
return createStepsProxy( stepSpyRef.current );
|
|
@@ -230,7 +253,7 @@ describe( 'workflow()', () => {
|
|
|
230
253
|
} );
|
|
231
254
|
|
|
232
255
|
describe( 'root workflow (in workflow context)', () => {
|
|
233
|
-
it( 'unwraps wrapped trace destinations and assigns
|
|
256
|
+
it( 'unwraps wrapped trace destinations and assigns traceInfo to memo', async () => {
|
|
234
257
|
traceDestinationsStepMock.mockResolvedValueOnce( {
|
|
235
258
|
output: { local: '/tmp/wrapped-trace' },
|
|
236
259
|
aggregations: null,
|
|
@@ -255,12 +278,15 @@ describe( 'workflow()', () => {
|
|
|
255
278
|
aggregations: null
|
|
256
279
|
} );
|
|
257
280
|
const memo = workflowInfoMock().memo;
|
|
258
|
-
expect( memo.
|
|
259
|
-
workflowId: '
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
startTime:
|
|
263
|
-
|
|
281
|
+
expect( memo.traceInfo ).toEqual( {
|
|
282
|
+
workflowId: 'trace-workflow-id',
|
|
283
|
+
workflowType: 'trace-workflow-type',
|
|
284
|
+
runId: 'trace-run-id',
|
|
285
|
+
startTime: 12345,
|
|
286
|
+
disableTrace: false
|
|
287
|
+
} );
|
|
288
|
+
expect( traceInfoBuildMock ).toHaveBeenCalledWith( { disableTrace: false } );
|
|
289
|
+
expect( traceDestinationsStepMock ).toHaveBeenCalledWith( memo.traceInfo );
|
|
264
290
|
} );
|
|
265
291
|
|
|
266
292
|
it( 'collects batched aggregation signals from failed activities', async () => {
|
|
@@ -290,7 +316,7 @@ describe( 'workflow()', () => {
|
|
|
290
316
|
expect( result.aggregations.httpRequests ).toEqual( { total: 1 } );
|
|
291
317
|
} );
|
|
292
318
|
|
|
293
|
-
it( 'sets
|
|
319
|
+
it( 'sets traceInfo.disableTrace when options.disableTrace is true', async () => {
|
|
294
320
|
const { workflow } = await import( './workflow.js' );
|
|
295
321
|
|
|
296
322
|
const wf = workflow( {
|
|
@@ -303,15 +329,23 @@ describe( 'workflow()', () => {
|
|
|
303
329
|
} );
|
|
304
330
|
|
|
305
331
|
await wf( {} );
|
|
306
|
-
expect( workflowInfoMock().memo.
|
|
332
|
+
expect( workflowInfoMock().memo.traceInfo ).toEqual( expect.objectContaining( {
|
|
333
|
+
workflowId: 'trace-workflow-id',
|
|
334
|
+
workflowType: 'trace-workflow-type',
|
|
335
|
+
runId: 'trace-run-id',
|
|
336
|
+
disableTrace: true
|
|
337
|
+
} ) );
|
|
338
|
+
expect( traceInfoBuildMock ).toHaveBeenCalledWith( { disableTrace: true } );
|
|
307
339
|
} );
|
|
308
340
|
} );
|
|
309
341
|
|
|
310
|
-
describe( 'child workflow (memo.
|
|
342
|
+
describe( 'child workflow (memo.traceInfo already set)', () => {
|
|
311
343
|
it( 'does not call getTraceDestinations and returns an internal output envelope', async () => {
|
|
344
|
+
const traceInfo = { workflowId: 'parent-1', workflowType: 'parent_wf', runId: 'parent-run' };
|
|
312
345
|
workflowInfoMock.mockReturnValue( {
|
|
313
346
|
...workflowInfoReturn,
|
|
314
|
-
|
|
347
|
+
root: { workflowId: 'root-wf', runId: 'root-run' },
|
|
348
|
+
memo: { traceInfo }
|
|
315
349
|
} );
|
|
316
350
|
const { workflow } = await import( './workflow.js' );
|
|
317
351
|
|
|
@@ -325,6 +359,7 @@ describe( 'workflow()', () => {
|
|
|
325
359
|
|
|
326
360
|
const result = await wf( {} );
|
|
327
361
|
expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
|
|
362
|
+
expect( workflowInfoMock().memo.traceInfo ).toBe( traceInfo );
|
|
328
363
|
expect( result ).toEqual( {
|
|
329
364
|
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
330
365
|
output: { x: 'child' },
|
|
@@ -445,10 +480,22 @@ describe( 'workflow()', () => {
|
|
|
445
480
|
workflowId: expect.stringMatching( /^wf-test-123-/ ),
|
|
446
481
|
parentClosePolicy: ParentClosePolicy.TERMINATE,
|
|
447
482
|
memo: expect.objectContaining( {
|
|
448
|
-
|
|
449
|
-
|
|
483
|
+
traceInfo: {
|
|
484
|
+
workflowId: 'trace-workflow-id',
|
|
485
|
+
workflowType: 'trace-workflow-type',
|
|
486
|
+
runId: 'trace-run-id',
|
|
487
|
+
startTime: 12345,
|
|
488
|
+
disableTrace: false
|
|
489
|
+
},
|
|
490
|
+
activityOptions: expect.objectContaining( {
|
|
491
|
+
startToCloseTimeout: '20m',
|
|
492
|
+
heartbeatTimeout: '5m'
|
|
493
|
+
} )
|
|
450
494
|
} )
|
|
451
495
|
} );
|
|
496
|
+
const [ , childOptions ] = executeChildMock.mock.calls[0];
|
|
497
|
+
expect( childOptions.memo ).not.toHaveProperty( 'executionContext' );
|
|
498
|
+
expect( childOptions.memo ).not.toHaveProperty( 'parentId' );
|
|
452
499
|
} );
|
|
453
500
|
|
|
454
501
|
it( 'uses ABANDON when extra.detached is true', async () => {
|
|
@@ -469,11 +516,12 @@ describe( 'workflow()', () => {
|
|
|
469
516
|
|
|
470
517
|
await wf( {} );
|
|
471
518
|
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
519
|
+
args: [ null ],
|
|
472
520
|
parentClosePolicy: ParentClosePolicy.ABANDON
|
|
473
521
|
} ) );
|
|
474
522
|
} );
|
|
475
523
|
|
|
476
|
-
it( 'passes empty args when input is
|
|
524
|
+
it( 'passes empty args when input is omitted', async () => {
|
|
477
525
|
const { workflow } = await import( './workflow.js' );
|
|
478
526
|
executeChildMock.mockResolvedValueOnce( { output: {}, aggregations: null } );
|
|
479
527
|
|
|
@@ -494,6 +542,47 @@ describe( 'workflow()', () => {
|
|
|
494
542
|
} ) );
|
|
495
543
|
} );
|
|
496
544
|
|
|
545
|
+
it( 'merges per-child activity options into the propagated memo', async () => {
|
|
546
|
+
const { workflow } = await import( './workflow.js' );
|
|
547
|
+
executeChildMock.mockResolvedValueOnce( { output: {}, aggregations: null } );
|
|
548
|
+
|
|
549
|
+
const wf = workflow( {
|
|
550
|
+
name: 'child_options_wf',
|
|
551
|
+
description: 'Child options',
|
|
552
|
+
inputSchema: z.object( {} ),
|
|
553
|
+
outputSchema: z.object( {} ),
|
|
554
|
+
async fn() {
|
|
555
|
+
await this.startWorkflow( 'child_wf', { id: 1 }, {
|
|
556
|
+
options: {
|
|
557
|
+
activityOptions: {
|
|
558
|
+
startToCloseTimeout: '2m',
|
|
559
|
+
retry: { maximumAttempts: 7 }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} );
|
|
563
|
+
return {};
|
|
564
|
+
}
|
|
565
|
+
} );
|
|
566
|
+
|
|
567
|
+
await wf( {} );
|
|
568
|
+
expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
|
|
569
|
+
memo: expect.objectContaining( {
|
|
570
|
+
traceInfo: expect.objectContaining( {
|
|
571
|
+
workflowId: 'trace-workflow-id',
|
|
572
|
+
runId: 'trace-run-id'
|
|
573
|
+
} ),
|
|
574
|
+
activityOptions: expect.objectContaining( {
|
|
575
|
+
startToCloseTimeout: '2m',
|
|
576
|
+
heartbeatTimeout: '5m',
|
|
577
|
+
retry: expect.objectContaining( {
|
|
578
|
+
initialInterval: '10s',
|
|
579
|
+
maximumAttempts: 7
|
|
580
|
+
} )
|
|
581
|
+
} )
|
|
582
|
+
} )
|
|
583
|
+
} ) );
|
|
584
|
+
} );
|
|
585
|
+
|
|
497
586
|
it( 'returns child output and merges child workflow aggregations into the root aggregations', async () => {
|
|
498
587
|
const { workflow } = await import( './workflow.js' );
|
|
499
588
|
executeChildMock.mockResolvedValueOnce( {
|
|
@@ -56,11 +56,11 @@ export const sendHttpRequest = async ( { url, method, payload = undefined, heade
|
|
|
56
56
|
setMetadata( sendHttpRequest, { type: ComponentType.INTERNAL_STEP } );
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Invokes a trace method that resolves all trace output paths based on the
|
|
59
|
+
* Invokes a trace method that resolves all trace output paths based on the traceInfo
|
|
60
60
|
*
|
|
61
|
-
* @param {object}
|
|
61
|
+
* @param {object} traceInfo
|
|
62
62
|
* @returns {object} Information about enabled destinations
|
|
63
63
|
*/
|
|
64
|
-
export const getTraceDestinations =
|
|
64
|
+
export const getTraceDestinations = traceInfo => getDestinations( traceInfo );
|
|
65
65
|
|
|
66
66
|
setMetadata( getTraceDestinations, { type: ComponentType.INTERNAL_STEP } );
|
|
@@ -2,7 +2,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { MockAgent, setGlobalDispatcher } from 'undici';
|
|
3
3
|
import { FatalError } from '#errors';
|
|
4
4
|
import { serializeBodyAndInferContentType, serializeFetchResponse } from '#utils';
|
|
5
|
-
import { sendHttpRequest } from './index.js';
|
|
5
|
+
import { getTraceDestinations, sendHttpRequest } from './index.js';
|
|
6
|
+
|
|
7
|
+
const getDestinationsMock = vi.hoisted( () => vi.fn() );
|
|
8
|
+
|
|
9
|
+
vi.mock( '#tracing', () => ( {
|
|
10
|
+
getDestinations: getDestinationsMock
|
|
11
|
+
} ) );
|
|
6
12
|
|
|
7
13
|
vi.mock( '#logger', () => {
|
|
8
14
|
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
@@ -100,3 +106,27 @@ describe( 'internal_activities/sendHttpRequest', () => {
|
|
|
100
106
|
expect( serializeBodyAndInferContentType ).not.toHaveBeenCalled();
|
|
101
107
|
} );
|
|
102
108
|
} );
|
|
109
|
+
|
|
110
|
+
describe( 'internal_activities/getTraceDestinations', () => {
|
|
111
|
+
beforeEach( () => {
|
|
112
|
+
vi.clearAllMocks();
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
it( 'returns trace destinations for the given traceInfo', () => {
|
|
116
|
+
const traceInfo = {
|
|
117
|
+
workflowId: 'workflow-id',
|
|
118
|
+
runId: 'run-id',
|
|
119
|
+
workflowType: 'workflow',
|
|
120
|
+
startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
|
|
121
|
+
disableTrace: false
|
|
122
|
+
};
|
|
123
|
+
const destinations = {
|
|
124
|
+
local: '/tmp/project/logs/runs/workflow/trace.json',
|
|
125
|
+
remote: null
|
|
126
|
+
};
|
|
127
|
+
getDestinationsMock.mockReturnValueOnce( destinations );
|
|
128
|
+
|
|
129
|
+
expect( getTraceDestinations( traceInfo ) ).toBe( destinations );
|
|
130
|
+
expect( getDestinationsMock ).toHaveBeenCalledWith( traceInfo );
|
|
131
|
+
} );
|
|
132
|
+
} );
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const createWorkflowDetails = info => ( {
|
|
2
|
+
attempt: info.attempt,
|
|
3
|
+
continuedFromExecutionRunId: info.continuedFromExecutionRunId,
|
|
4
|
+
firstExecutionRunId: info.firstExecutionRunId,
|
|
5
|
+
parent: info.parent,
|
|
6
|
+
root: info.root,
|
|
7
|
+
runId: info.runId,
|
|
8
|
+
runStartTime: info.runStartTime.getTime(),
|
|
9
|
+
startTime: info.startTime.getTime(),
|
|
10
|
+
workflowId: info.workflowId,
|
|
11
|
+
workflowType: info.workflowType
|
|
12
|
+
} );
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { WorkflowInfo } from '@temporalio/workflow';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createWorkflowDetails } from './temporal_context.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
This spec is TypeScript on purpose.
|
|
7
|
+
createWorkflowDetails() accepts a small projection of Temporal's WorkflowInfo,
|
|
8
|
+
so the fixture below uses Pick<WorkflowInfo, ...> to catch Temporal type drift
|
|
9
|
+
without exposing or maintaining the full native object in hook payloads.
|
|
10
|
+
*/
|
|
11
|
+
type WorkflowDetailsSource = Pick<
|
|
12
|
+
WorkflowInfo,
|
|
13
|
+
'attempt' |
|
|
14
|
+
'continuedFromExecutionRunId' |
|
|
15
|
+
'firstExecutionRunId' |
|
|
16
|
+
'parent' |
|
|
17
|
+
'root' |
|
|
18
|
+
'runId' |
|
|
19
|
+
'runStartTime' |
|
|
20
|
+
'startTime' |
|
|
21
|
+
'workflowId' |
|
|
22
|
+
'workflowType'
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
describe( 'createWorkflowDetails', () => {
|
|
26
|
+
it( 'creates hook-safe workflow details from Temporal workflow info', () => {
|
|
27
|
+
const parent = { workflowId: 'parent-wf', runId: 'parent-run', namespace: 'default' };
|
|
28
|
+
const root = { workflowId: 'root-wf', runId: 'root-run' };
|
|
29
|
+
const workflowInfo = {
|
|
30
|
+
attempt: 2,
|
|
31
|
+
continuedFromExecutionRunId: 'previous-run',
|
|
32
|
+
firstExecutionRunId: 'first-run',
|
|
33
|
+
parent,
|
|
34
|
+
root,
|
|
35
|
+
runId: 'current-run',
|
|
36
|
+
runStartTime: new Date( '2026-06-02T09:30:00.000Z' ),
|
|
37
|
+
startTime: new Date( '2026-06-02T09:00:00.000Z' ),
|
|
38
|
+
workflowId: 'workflow-id',
|
|
39
|
+
workflowType: 'prompt'
|
|
40
|
+
} satisfies WorkflowDetailsSource;
|
|
41
|
+
|
|
42
|
+
expect( createWorkflowDetails( workflowInfo ) ).toEqual( {
|
|
43
|
+
attempt: 2,
|
|
44
|
+
continuedFromExecutionRunId: 'previous-run',
|
|
45
|
+
firstExecutionRunId: 'first-run',
|
|
46
|
+
parent,
|
|
47
|
+
root,
|
|
48
|
+
runId: 'current-run',
|
|
49
|
+
runStartTime: Date.parse( '2026-06-02T09:30:00.000Z' ),
|
|
50
|
+
startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
|
|
51
|
+
workflowId: 'workflow-id',
|
|
52
|
+
workflowType: 'prompt'
|
|
53
|
+
} );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'preserves absent optional workflow relationships as undefined', () => {
|
|
57
|
+
const workflowInfo = {
|
|
58
|
+
attempt: 1,
|
|
59
|
+
continuedFromExecutionRunId: undefined,
|
|
60
|
+
firstExecutionRunId: 'first-run',
|
|
61
|
+
parent: undefined,
|
|
62
|
+
root: undefined,
|
|
63
|
+
runId: 'current-run',
|
|
64
|
+
runStartTime: new Date( '2026-06-02T09:30:00.000Z' ),
|
|
65
|
+
startTime: new Date( '2026-06-02T09:00:00.000Z' ),
|
|
66
|
+
workflowId: 'workflow-id',
|
|
67
|
+
workflowType: 'prompt'
|
|
68
|
+
} satisfies WorkflowDetailsSource;
|
|
69
|
+
|
|
70
|
+
expect( createWorkflowDetails( workflowInfo ) ).toEqual( {
|
|
71
|
+
attempt: 1,
|
|
72
|
+
continuedFromExecutionRunId: undefined,
|
|
73
|
+
firstExecutionRunId: 'first-run',
|
|
74
|
+
parent: undefined,
|
|
75
|
+
root: undefined,
|
|
76
|
+
runId: 'current-run',
|
|
77
|
+
runStartTime: Date.parse( '2026-06-02T09:30:00.000Z' ),
|
|
78
|
+
startTime: Date.parse( '2026-06-02T09:00:00.000Z' ),
|
|
79
|
+
workflowId: 'workflow-id',
|
|
80
|
+
workflowType: 'prompt'
|
|
81
|
+
} );
|
|
82
|
+
} );
|
|
83
|
+
} );
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { inWorkflowContext, workflowInfo } from '@temporalio/workflow';
|
|
2
|
+
|
|
3
|
+
export class TraceInfo {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds the trace information propagated through workflow memo and activity headers.
|
|
7
|
+
* @param {object} options - Arguments to build trace information
|
|
8
|
+
* @param {boolean} options.disableTrace - Whether trace event emission should be disabled
|
|
9
|
+
* @returns {object} trace information
|
|
10
|
+
*/
|
|
11
|
+
static build( { disableTrace } ) {
|
|
12
|
+
const info = inWorkflowContext() ? workflowInfo() : {};
|
|
13
|
+
return {
|
|
14
|
+
workflowId: info.workflowId,
|
|
15
|
+
workflowType: info.workflowType,
|
|
16
|
+
runId: info.runId,
|
|
17
|
+
startTime: info.startTime?.getTime(),
|
|
18
|
+
disableTrace
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|