@outputai/core 0.6.0 → 0.6.1-next.383b24b.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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/activity_integration/context.d.ts +5 -9
  3. package/src/activity_integration/context.js +5 -4
  4. package/src/activity_integration/context.spec.js +10 -15
  5. package/src/activity_integration/events.d.ts +2 -4
  6. package/src/activity_integration/events.js +8 -3
  7. package/src/activity_integration/events.spec.js +58 -29
  8. package/src/bus.js +18 -9
  9. package/src/bus.spec.js +30 -0
  10. package/src/hooks/index.d.ts +112 -58
  11. package/src/hooks/index.js +15 -12
  12. package/src/hooks/index.spec.js +60 -32
  13. package/src/interface/workflow.js +19 -35
  14. package/src/interface/workflow.spec.js +104 -15
  15. package/src/internal_activities/index.js +3 -3
  16. package/src/internal_activities/index.spec.js +31 -1
  17. package/src/internal_utils/temporal_context.js +12 -0
  18. package/src/internal_utils/temporal_context.spec.ts +83 -0
  19. package/src/internal_utils/trace_info.js +21 -0
  20. package/src/internal_utils/trace_info.spec.js +47 -0
  21. package/src/internal_utils/workflow_context.js +29 -0
  22. package/src/internal_utils/workflow_context.spec.js +46 -0
  23. package/src/tracing/internal_interface.js +4 -4
  24. package/src/tracing/processors/local/index.js +21 -26
  25. package/src/tracing/processors/local/index.spec.js +39 -45
  26. package/src/tracing/processors/s3/index.js +13 -23
  27. package/src/tracing/processors/s3/index.spec.js +33 -26
  28. package/src/tracing/trace_attribute.js +0 -1
  29. package/src/tracing/trace_engine.js +8 -12
  30. package/src/tracing/trace_engine.spec.js +31 -27
  31. package/src/worker/interceptors/activity.js +31 -29
  32. package/src/worker/interceptors/activity.spec.js +58 -26
  33. package/src/worker/interceptors/workflow.js +7 -2
  34. package/src/worker/interceptors/workflow.spec.js +42 -6
  35. package/src/worker/log_hooks.js +35 -46
  36. package/src/worker/log_hooks.spec.js +43 -46
  37. package/src/worker/sinks.js +24 -24
  38. package/src/interface/workflow_context.js +0 -33
@@ -32,16 +32,12 @@ const processors = [
32
32
  /**
33
33
  * Returns the destinations for a given execution context
34
34
  *
35
- * @param {object} executionContext
36
- * @param {string} executionContext.startTime
37
- * @param {string} executionContext.workflowId
38
- * @param {string} executionContext.workflowName
39
- * @param {boolean} executionContext.disableTrace
35
+ * @param {object} traceInfo - The trace information object
40
36
  * @returns {object} A trace destinations object: { [dest-name]: 'path' }
41
37
  */
42
- export const getDestinations = executionContext =>
38
+ export const getDestinations = traceInfo =>
43
39
  processors.reduce( ( o, p ) =>
44
- Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !executionContext.disableTrace ? p.getDestination( executionContext ) : null } )
40
+ Object.assign( o, { [p.name.toLowerCase()]: p.enabled && !traceInfo.disableTrace ? p.getDestination( traceInfo ) : null } )
45
41
  , {} );
46
42
 
47
43
  /**
@@ -72,11 +68,11 @@ const serializeDetails = details => details instanceof Error ? serializeError( d
72
68
  * @param {object} fields - All the trace fields
73
69
  * @returns {void}
74
70
  */
75
- export const addEventAction = ( action, { kind, name, id, parentId, details, executionContext } ) => {
71
+ export const addEventAction = ( action, { kind, name, id, parentId, details, traceInfo } ) => {
76
72
  // Ignores internal steps in the actual trace files, ignore trace if the flag is true
77
- if ( kind !== ComponentType.INTERNAL_STEP && !executionContext.disableTrace ) {
73
+ if ( kind !== ComponentType.INTERNAL_STEP && !traceInfo.disableTrace ) {
78
74
  traceBus.emit( 'entry', {
79
- executionContext,
75
+ traceInfo,
80
76
  entry: { kind, action, name, id, parentId, timestamp: Date.now(), details: serializeDetails( details ) }
81
77
  } );
82
78
  }
@@ -93,7 +89,7 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, exe
93
89
  export function addEventActionWithContext( action, options ) {
94
90
  const storeContent = Storage.load();
95
91
  if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
96
- const { parentId, executionContext, addAttribute } = storeContent;
92
+ const { parentId, traceInfo, addAttribute } = storeContent;
97
93
  if ( action === EventAction.ADD_ATTR ) {
98
94
  const attribute = options.details;
99
95
  if ( !( attribute instanceof BaseAttribute ) ) {
@@ -102,6 +98,6 @@ export function addEventActionWithContext( action, options ) {
102
98
  addAttribute( options.details );
103
99
  }
104
100
  }
105
- addEventAction( action, { ...options, parentId, executionContext } );
101
+ addEventAction( action, { ...options, parentId, traceInfo } );
106
102
  }
107
103
  };
@@ -34,6 +34,14 @@ async function loadTraceEngine() {
34
34
  return import( './trace_engine.js' );
35
35
  }
36
36
 
37
+ const traceInfo = {
38
+ workflowId: 'w1',
39
+ runId: 'r1',
40
+ workflowType: 'WF',
41
+ startTime: 1,
42
+ disableTrace: false
43
+ };
44
+
37
45
  describe( 'tracing/trace_engine', () => {
38
46
  beforeEach( () => {
39
47
  vi.clearAllMocks();
@@ -52,9 +60,8 @@ describe( 'tracing/trace_engine', () => {
52
60
  expect( localInitMock ).toHaveBeenCalledTimes( 1 );
53
61
  expect( s3InitMock ).not.toHaveBeenCalled();
54
62
 
55
- const executionContext = { disableTrace: false };
56
63
  addEventAction( 'start', {
57
- kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, executionContext
64
+ kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, traceInfo
58
65
  } );
59
66
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
60
67
  const payload = localExecMock.mock.calls[0][0];
@@ -62,7 +69,7 @@ describe( 'tracing/trace_engine', () => {
62
69
  expect( payload.entry.kind ).toBe( 'step' );
63
70
  expect( payload.entry.action ).toBe( 'start' );
64
71
  expect( payload.entry.details ).toEqual( { ok: true } );
65
- expect( payload.executionContext ).toBe( executionContext );
72
+ expect( payload.traceInfo ).toBe( traceInfo );
66
73
  } );
67
74
 
68
75
  it( 'addEventAction() emits an entry consumed by processors', async () => {
@@ -72,7 +79,7 @@ describe( 'tracing/trace_engine', () => {
72
79
 
73
80
  addEventAction( 'end', {
74
81
  kind: 'workflow', name: 'W', id: '2', parentId: 'p2', details: 'done',
75
- executionContext: { disableTrace: false }
82
+ traceInfo
76
83
  } );
77
84
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
78
85
  const payload = localExecMock.mock.calls[0][0];
@@ -81,14 +88,14 @@ describe( 'tracing/trace_engine', () => {
81
88
  expect( payload.entry.details ).toBe( 'done' );
82
89
  } );
83
90
 
84
- it( 'addEventAction() does not emit when executionContext.disableTrace is true', async () => {
91
+ it( 'addEventAction() does not emit when traceInfo.disableTrace is true', async () => {
85
92
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
86
93
  const { init, addEventAction } = await loadTraceEngine();
87
94
  await init();
88
95
 
89
96
  addEventAction( 'start', {
90
97
  kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
91
- executionContext: { disableTrace: true }
98
+ traceInfo: { ...traceInfo, disableTrace: true }
92
99
  } );
93
100
  expect( localExecMock ).not.toHaveBeenCalled();
94
101
  } );
@@ -100,7 +107,7 @@ describe( 'tracing/trace_engine', () => {
100
107
 
101
108
  addEventAction( 'start', {
102
109
  kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
103
- executionContext: { disableTrace: false }
110
+ traceInfo
104
111
  } );
105
112
  expect( localExecMock ).not.toHaveBeenCalled();
106
113
  } );
@@ -109,7 +116,7 @@ describe( 'tracing/trace_engine', () => {
109
116
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
110
117
  storageLoadMock.mockReturnValue( {
111
118
  parentId: 'ctx-p',
112
- executionContext: { runId: 'r1', disableTrace: false }
119
+ traceInfo
113
120
  } );
114
121
  const { init, addEventActionWithContext } = await loadTraceEngine();
115
122
  await init();
@@ -117,7 +124,7 @@ describe( 'tracing/trace_engine', () => {
117
124
  addEventActionWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
118
125
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
119
126
  const payload = localExecMock.mock.calls[0][0];
120
- expect( payload.executionContext ).toEqual( { runId: 'r1', disableTrace: false } );
127
+ expect( payload.traceInfo ).toBe( traceInfo );
121
128
  expect( payload.entry.parentId ).toBe( 'ctx-p' );
122
129
  expect( payload.entry.name ).toBe( 'S' );
123
130
  expect( payload.entry.action ).toBe( 'tick' );
@@ -126,10 +133,9 @@ describe( 'tracing/trace_engine', () => {
126
133
  it( 'addEventActionWithContext() records ADD_ATTR attributes through storage context', async () => {
127
134
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
128
135
  const addAttributeMock = vi.fn();
129
- const executionContext = { runId: 'r1', disableTrace: false };
130
136
  storageLoadMock.mockReturnValue( {
131
137
  parentId: 'ctx-p',
132
- executionContext,
138
+ traceInfo,
133
139
  addAttribute: addAttributeMock
134
140
  } );
135
141
  const { init, addEventActionWithContext } = await loadTraceEngine();
@@ -144,7 +150,7 @@ describe( 'tracing/trace_engine', () => {
144
150
  expect( addAttributeMock ).toHaveBeenCalledWith( attribute );
145
151
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
146
152
  expect( localExecMock.mock.calls[0][0] ).toEqual( {
147
- executionContext,
153
+ traceInfo,
148
154
  entry: {
149
155
  kind: 'http',
150
156
  action: EventAction.ADD_ATTR,
@@ -162,7 +168,7 @@ describe( 'tracing/trace_engine', () => {
162
168
  const addAttributeMock = vi.fn();
163
169
  storageLoadMock.mockReturnValue( {
164
170
  parentId: 'ctx-p',
165
- executionContext: { runId: 'r1', disableTrace: false },
171
+ traceInfo,
166
172
  addAttribute: addAttributeMock
167
173
  } );
168
174
  const { init, addEventActionWithContext } = await loadTraceEngine();
@@ -179,11 +185,11 @@ describe( 'tracing/trace_engine', () => {
179
185
  expect( localExecMock ).not.toHaveBeenCalled();
180
186
  } );
181
187
 
182
- it( 'addEventActionWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
188
+ it( 'addEventActionWithContext() does not emit when storage traceInfo.disableTrace is true', async () => {
183
189
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
184
190
  storageLoadMock.mockReturnValue( {
185
191
  parentId: 'ctx-p',
186
- executionContext: { runId: 'r1', disableTrace: true }
192
+ traceInfo: { ...traceInfo, disableTrace: true }
187
193
  } );
188
194
  const { init, addEventActionWithContext } = await loadTraceEngine();
189
195
  await init();
@@ -203,21 +209,19 @@ describe( 'tracing/trace_engine', () => {
203
209
  } );
204
210
 
205
211
  describe( 'getDestinations()', () => {
206
- const executionContext = { workflowId: 'w1', workflowName: 'WF', startTime: 1, disableTrace: false };
207
-
208
212
  it( 'returns null for both when traces are off (env vars unset)', async () => {
209
213
  const { getDestinations } = await loadTraceEngine();
210
- const result = getDestinations( executionContext );
214
+ const result = getDestinations( traceInfo );
211
215
  expect( result ).toEqual( { local: null, remote: null } );
212
216
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
213
217
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
214
218
  } );
215
219
 
216
- it( 'returns null for both when executionContext.disableTrace is true', async () => {
220
+ it( 'returns null for both when traceInfo.disableTrace is true', async () => {
217
221
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
218
222
  process.env.OUTPUT_TRACE_REMOTE_ON = '1';
219
223
  const { getDestinations } = await loadTraceEngine();
220
- const result = getDestinations( { ...executionContext, disableTrace: true } );
224
+ const result = getDestinations( { ...traceInfo, disableTrace: true } );
221
225
  expect( result ).toEqual( { local: null, remote: null } );
222
226
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
223
227
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
@@ -227,24 +231,24 @@ describe( 'tracing/trace_engine', () => {
227
231
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
228
232
  process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
229
233
  const { getDestinations } = await loadTraceEngine();
230
- const result = getDestinations( executionContext );
234
+ const result = getDestinations( traceInfo );
231
235
  expect( result ).toEqual( {
232
236
  local: '/local/path.json',
233
237
  remote: 'https://bucket.s3.amazonaws.com/key.json'
234
238
  } );
235
239
  expect( localGetDestinationMock ).toHaveBeenCalledTimes( 1 );
236
- expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
240
+ expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
237
241
  expect( s3GetDestinationMock ).toHaveBeenCalledTimes( 1 );
238
- expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
242
+ expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
239
243
  } );
240
244
 
241
245
  it( 'returns local only when local trace on and remote off', async () => {
242
246
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
243
247
  process.env.OUTPUT_TRACE_REMOTE_ON = '0';
244
248
  const { getDestinations } = await loadTraceEngine();
245
- const result = getDestinations( executionContext );
249
+ const result = getDestinations( traceInfo );
246
250
  expect( result ).toEqual( { local: '/local/path.json', remote: null } );
247
- expect( localGetDestinationMock ).toHaveBeenCalledWith( executionContext );
251
+ expect( localGetDestinationMock ).toHaveBeenCalledWith( traceInfo );
248
252
  expect( s3GetDestinationMock ).not.toHaveBeenCalled();
249
253
  } );
250
254
 
@@ -252,10 +256,10 @@ describe( 'tracing/trace_engine', () => {
252
256
  process.env.OUTPUT_TRACE_LOCAL_ON = '0';
253
257
  process.env.OUTPUT_TRACE_REMOTE_ON = 'true';
254
258
  const { getDestinations } = await loadTraceEngine();
255
- const result = getDestinations( executionContext );
259
+ const result = getDestinations( traceInfo );
256
260
  expect( result ).toEqual( { local: null, remote: 'https://bucket.s3.amazonaws.com/key.json' } );
257
261
  expect( localGetDestinationMock ).not.toHaveBeenCalled();
258
- expect( s3GetDestinationMock ).toHaveBeenCalledWith( executionContext );
262
+ expect( s3GetDestinationMock ).toHaveBeenCalledWith( traceInfo );
259
263
  } );
260
264
  } );
261
265
  } );
@@ -1,4 +1,4 @@
1
- import { Context } from '@temporalio/activity';
1
+ import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
2
2
  import { Storage } from '#async_storage';
3
3
  import * as Tracing from '#tracing';
4
4
  import { headersToObject } from '../sandboxed_utils.js';
@@ -25,7 +25,7 @@ const log = createChildLogger( 'ActivityInterceptor' );
25
25
 
26
26
  Context information comes from two sources:
27
27
  - Temporal's Activity Context (workflowId, activityId, activityType)
28
- - Headers injected by the workflow interceptor (executionContext)
28
+ - Headers injected by the workflow interceptor
29
29
  */
30
30
  export class ActivityExecutionInterceptor {
31
31
  constructor( { activities, workflows, connection } ) {
@@ -42,25 +42,24 @@ export class ActivityExecutionInterceptor {
42
42
 
43
43
  /**
44
44
  * Returns a workflow entry by its name or throws error
45
- * @param {string} workflowName
45
+ * @param {string} workflowType
46
46
  * @returns {object} Workflow entry
47
47
  * @throws {Error}
48
48
  */
49
- getWorkflowEntry( workflowName ) {
50
- const workflowEntry = this.workflowsMap.get( workflowName );
49
+ getWorkflowEntry( workflowType ) {
50
+ const workflowEntry = this.workflowsMap.get( workflowType );
51
51
  if ( !workflowEntry ) {
52
- throw new Error( `Activity interceptor: workflow "${workflowName}" not found in workflowsMap.` );
52
+ throw new Error( `Activity interceptor: workflow "${workflowType}" not found in workflowsMap.` );
53
53
  }
54
54
  return workflowEntry;
55
55
  }
56
56
 
57
57
  async execute( input, next ) {
58
- const startDate = Date.now();
59
-
60
- const { workflowExecution: { workflowId }, activityId: id, activityType: name, workflowType: workflowName } = Context.current().info;
61
- const { executionContext } = headersToObject( input.headers );
62
- const { type: kind } = this.activities?.[name]?.[METADATA_ACCESS_SYMBOL];
63
- const { path: workflowFilename } = this.getWorkflowEntry( workflowName );
58
+ const activityInfo = activityInfoFn();
59
+ const { workflowExecution: { workflowId, runId }, activityId, activityType, workflowType } = activityInfo;
60
+ const { traceInfo, workflowDetails } = headersToObject( input.headers );
61
+ const { type: outputActivityKind } = this.activities?.[activityType]?.[METADATA_ACCESS_SYMBOL];
62
+ const { path: workflowFilename } = this.getWorkflowEntry( workflowType );
64
63
 
65
64
  const state = {
66
65
  heartbeat: null,
@@ -76,32 +75,35 @@ export class ActivityExecutionInterceptor {
76
75
  const workflowHandle = client.workflow.getHandle( workflowId );
77
76
  await workflowHandle.signal( Signal.SEND_AGGREGATIONS, aggregateAttributes( state.attributes ) );
78
77
  } catch ( error ) {
79
- log.warn( `Signal "${Signal.SEND_AGGREGATIONS}" failed`, {
80
- message: error.message,
81
- stack: error.stack,
82
- activityId: id,
83
- activityName: name,
84
- workflowId,
85
- workflowName
86
- } );
78
+ const errorContext = { message: error.message, stack: error.stack, activityId, activityType, workflowId, workflowType, runId };
79
+ log.warn( `Signal "${Signal.SEND_AGGREGATIONS}" failed`, errorContext );
87
80
  }
88
81
  }
89
82
  };
90
83
 
91
- // Wraps the execution with accessible metadata for the activity
92
- const ctx = { parentId: id, executionContext, workflowFilename, addAttribute };
84
+ // Adds context accessible information
85
+ const storageContext = {
86
+ parentId: activityId,
87
+ outputActivityKind,
88
+ activityInfo,
89
+ workflowDetails,
90
+ workflowFilename,
91
+ traceInfo,
92
+ addAttribute
93
+ };
93
94
 
94
- messageBus.emit( BusEventType.ACTIVITY_START, { id, name, kind, workflowId, workflowName } );
95
- Tracing.addEventStart( { id, name, kind, parentId: workflowId, details: input.args[0], executionContext } );
95
+ messageBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
96
+ Tracing.addEventStart( { id: activityId, name: activityType, kind: outputActivityKind, parentId: runId, details: input.args[0], traceInfo } );
96
97
 
97
98
  try {
98
99
  // Sends heartbeat to communicate that activity is still alive
99
100
  state.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
100
101
 
101
- const output = await Storage.runWithContext( async _ => next( input ), ctx );
102
+ const output = await Storage.runWithContext( async _ => next( input ), storageContext );
103
+
104
+ messageBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
105
+ Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
102
106
 
103
- messageBus.emit( BusEventType.ACTIVITY_END, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate } );
104
- Tracing.addEventEnd( { id, details: output, executionContext } );
105
107
  return {
106
108
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1,
107
109
  output,
@@ -109,8 +111,8 @@ export class ActivityExecutionInterceptor {
109
111
  };
110
112
 
111
113
  } catch ( error ) {
112
- messageBus.emit( BusEventType.ACTIVITY_ERROR, { id, name, kind, workflowId, workflowName, duration: Date.now() - startDate, error } );
113
- Tracing.addEventError( { id, details: error, executionContext } );
114
+ messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error } );
115
+ Tracing.addEventError( { id: activityId, details: error, traceInfo } );
114
116
 
115
117
  await sendAggregationsViaSignal();
116
118
 
@@ -7,20 +7,36 @@ const workflowHandleMock = vi.hoisted( () => ( { signal: vi.fn() } ) );
7
7
  const getHandleMock = vi.hoisted( () => vi.fn( () => workflowHandleMock ) );
8
8
  const clientConstructorMock = vi.hoisted( () => vi.fn() );
9
9
  const logWarnMock = vi.hoisted( () => vi.fn() );
10
-
11
- const heartbeatMock = vi.fn();
10
+ const heartbeatMock = vi.hoisted( () => vi.fn() );
12
11
  const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
13
- const contextInfoMock = {
14
- workflowExecution: { workflowId: 'wf-1' },
12
+ const activityInfoMock = vi.hoisted( () => ( {
13
+ workflowExecution: { workflowId: 'wf-1', runId: 'run-1' },
15
14
  activityId: 'act-1',
16
15
  activityType: 'myWorkflow#myStep',
17
16
  workflowType: 'myWorkflow'
18
- };
17
+ } ) );
18
+ const traceInfoMock = vi.hoisted( () => ( {
19
+ workflowId: 'wf-1',
20
+ runId: 'run-1',
21
+ workflowType: 'myWorkflow',
22
+ startTime: 1710000000000,
23
+ disableTrace: false
24
+ } ) );
25
+ const workflowDetailsMock = vi.hoisted( () => ( {
26
+ workflowId: 'wf-1',
27
+ runId: 'run-1',
28
+ workflowType: 'myWorkflow',
29
+ firstExecutionRunId: 'run-1',
30
+ startTime: 1710000000000,
31
+ runStartTime: 1710000000000,
32
+ attempt: 1
33
+ } ) );
19
34
 
20
35
  vi.mock( '@temporalio/activity', () => ( {
36
+ activityInfo: () => activityInfoMock,
21
37
  Context: {
22
38
  current: () => ( {
23
- info: contextInfoMock,
39
+ info: activityInfoMock,
24
40
  heartbeat: heartbeatMock
25
41
  } )
26
42
  }
@@ -58,7 +74,7 @@ vi.mock( '#tracing', () => ( {
58
74
  } ) );
59
75
 
60
76
  vi.mock( '../sandboxed_utils.js', () => ( {
61
- headersToObject: () => ( { executionContext: { workflowId: 'wf-1' } } )
77
+ headersToObject: () => ( { traceInfo: traceInfoMock, workflowDetails: workflowDetailsMock } )
62
78
  } ) );
63
79
 
64
80
  const messageBusEmitMock = vi.fn();
@@ -105,6 +121,7 @@ const httpRequestAttribute = {
105
121
  describe( 'ActivityExecutionInterceptor', () => {
106
122
  beforeEach( () => {
107
123
  vi.clearAllMocks();
124
+ activityInfoMock.workflowType = 'myWorkflow';
108
125
  workflowHandleMock.signal.mockResolvedValue( undefined );
109
126
  vi.useFakeTimers();
110
127
  vi.resetModules();
@@ -132,22 +149,34 @@ describe( 'ActivityExecutionInterceptor', () => {
132
149
  aggregations: null,
133
150
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
134
151
  } );
135
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.objectContaining( {
136
- id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow'
137
- } ) );
138
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.objectContaining( {
139
- id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow', duration: expect.any( Number )
140
- } ) );
141
- expect( addEventStartMock ).toHaveBeenCalledOnce();
142
- expect( addEventEndMock ).toHaveBeenCalledOnce();
152
+ expect( messageBusEmitMock ).toHaveBeenCalledWith(
153
+ BusEventType.ACTIVITY_START,
154
+ { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
155
+ );
156
+ expect( messageBusEmitMock ).toHaveBeenCalledWith(
157
+ BusEventType.ACTIVITY_END,
158
+ { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
159
+ );
160
+ expect( addEventStartMock ).toHaveBeenCalledWith( {
161
+ id: 'act-1',
162
+ name: 'myWorkflow#myStep',
163
+ kind: 'step',
164
+ parentId: 'run-1',
165
+ details: { someInput: 'data' },
166
+ traceInfo: traceInfoMock
167
+ } );
168
+ expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'ok' }, traceInfo: traceInfoMock } );
143
169
  expect( addEventErrorMock ).not.toHaveBeenCalled();
144
170
  expect( clientConstructorMock ).not.toHaveBeenCalled();
145
171
  expect( runWithContextMock ).toHaveBeenCalledWith(
146
172
  expect.any( Function ),
147
173
  expect.objectContaining( {
148
174
  parentId: 'act-1',
149
- executionContext: { workflowId: 'wf-1' },
175
+ activityInfo: activityInfoMock,
176
+ workflowDetails: workflowDetailsMock,
177
+ outputActivityKind: 'step',
150
178
  workflowFilename: '/workflows/myWorkflow.js',
179
+ traceInfo: traceInfoMock,
151
180
  addAttribute: expect.any( Function )
152
181
  } )
153
182
  );
@@ -166,7 +195,7 @@ describe( 'ActivityExecutionInterceptor', () => {
166
195
  } );
167
196
 
168
197
  expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
169
- expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, executionContext: { workflowId: 'wf-1' } } );
198
+ expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, traceInfo: traceInfoMock } );
170
199
  expect( addEventErrorMock ).not.toHaveBeenCalled();
171
200
  } );
172
201
 
@@ -244,9 +273,10 @@ describe( 'ActivityExecutionInterceptor', () => {
244
273
  expect( logWarnMock ).toHaveBeenCalledWith( `Signal "${Signal.SEND_AGGREGATIONS}" failed`, expect.objectContaining( {
245
274
  message: 'signal failed',
246
275
  activityId: 'act-1',
247
- activityName: 'myWorkflow#myStep',
276
+ activityType: 'myWorkflow#myStep',
248
277
  workflowId: 'wf-1',
249
- workflowName: 'myWorkflow'
278
+ workflowType: 'myWorkflow',
279
+ runId: 'run-1'
250
280
  } ) );
251
281
  } );
252
282
 
@@ -261,12 +291,14 @@ describe( 'ActivityExecutionInterceptor', () => {
261
291
 
262
292
  await expect( promise ).rejects.toThrow( 'step failed' );
263
293
  expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
264
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.objectContaining( {
265
- id: 'act-1', name: 'myWorkflow#myStep', kind: 'step', workflowId: 'wf-1', workflowName: 'myWorkflow',
266
- duration: expect.any( Number ), error: expect.any( Error )
267
- } ) );
294
+ expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
295
+ activityInfo: activityInfoMock,
296
+ workflowDetails: workflowDetailsMock,
297
+ outputActivityKind: 'step',
298
+ error
299
+ } );
268
300
  expect( addEventStartMock ).toHaveBeenCalledOnce();
269
- expect( addEventErrorMock ).toHaveBeenCalledOnce();
301
+ expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
270
302
  expect( addEventEndMock ).not.toHaveBeenCalled();
271
303
  } );
272
304
 
@@ -331,7 +363,7 @@ describe( 'ActivityExecutionInterceptor', () => {
331
363
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows } );
332
364
 
333
365
  // Override context to use alias as workflowType
334
- contextInfoMock.workflowType = 'myWorkflowOld';
366
+ activityInfoMock.workflowType = 'myWorkflowOld';
335
367
  const next = vi.fn().mockResolvedValue( { result: 'ok' } );
336
368
 
337
369
  const promise = interceptor.execute( makeInput(), next );
@@ -345,7 +377,7 @@ describe( 'ActivityExecutionInterceptor', () => {
345
377
  );
346
378
 
347
379
  // Restore for other tests
348
- contextInfoMock.workflowType = 'myWorkflow';
380
+ activityInfoMock.workflowType = 'myWorkflow';
349
381
  } );
350
382
 
351
383
  it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
@@ -5,6 +5,7 @@ import { deepMerge } from '#utils';
5
5
  import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
6
6
  // this is a dynamic generated file with activity configs overwrites
7
7
  import stepOptions from '../temp/__activity_options.js';
8
+ import { createWorkflowDetails } from '#internal_utils/temporal_context';
8
9
 
9
10
  /*
10
11
  This is not an AI comment!
@@ -17,8 +18,12 @@ import stepOptions from '../temp/__activity_options.js';
17
18
  */
18
19
  class HeadersInjectionInterceptor {
19
20
  async scheduleActivity( input, next ) {
20
- const memo = workflowInfo().memo ?? {};
21
- Object.assign( input.headers, memoToHeaders( memo ) );
21
+ const info = workflowInfo();
22
+ const memo = info.memo ?? {};
23
+ Object.assign( input.headers, memoToHeaders( {
24
+ ...memo,
25
+ workflowDetails: createWorkflowDetails( info )
26
+ } ) );
22
27
  // apply per-invocation options passed as second argument by rewritten calls
23
28
  const options = stepOptions[input.activityType];
24
29
  if ( options ) {
@@ -7,6 +7,35 @@ const workflowStartMock = vi.fn();
7
7
  const workflowEndMock = vi.fn();
8
8
  const workflowErrorMock = vi.fn();
9
9
  const isCancellationMock = vi.fn();
10
+ const startTime = new Date( '2026-06-02T09:00:00.000Z' );
11
+ const runStartTime = new Date( '2026-06-02T09:05:00.000Z' );
12
+ const workflowDetails = {
13
+ attempt: 1,
14
+ continuedFromExecutionRunId: undefined,
15
+ firstExecutionRunId: 'first-run',
16
+ parent: undefined,
17
+ root: undefined,
18
+ runId: 'run-1',
19
+ runStartTime: runStartTime.getTime(),
20
+ startTime: startTime.getTime(),
21
+ workflowId: 'workflow-1',
22
+ workflowType: 'MyWorkflow'
23
+ };
24
+
25
+ const workflowInfo = {
26
+ attempt: 1,
27
+ continuedFromExecutionRunId: undefined,
28
+ firstExecutionRunId: 'first-run',
29
+ parent: undefined,
30
+ root: undefined,
31
+ runId: 'run-1',
32
+ runStartTime,
33
+ startTime,
34
+ workflowId: 'workflow-1',
35
+ workflowType: 'MyWorkflow',
36
+ memo: { traceInfo: { runId: 'root-run' } }
37
+ };
38
+
10
39
  vi.mock( '@temporalio/workflow', () => ( {
11
40
  workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
12
41
  proxySinks: () => ( {
@@ -53,7 +82,7 @@ describe( 'workflow interceptors', () => {
53
82
  beforeEach( () => {
54
83
  vi.clearAllMocks();
55
84
  isCancellationMock.mockReturnValue( false );
56
- workflowInfoMock.mockReturnValue( { workflowType: 'MyWorkflow', memo: { executionContext: { id: 'ctx-1' } } } );
85
+ workflowInfoMock.mockReturnValue( workflowInfo );
57
86
  } );
58
87
 
59
88
  describe( 'HeadersInjectionInterceptor', () => {
@@ -64,12 +93,19 @@ describe( 'workflow interceptors', () => {
64
93
  const input = { headers: { existing: 'header' }, activityType: 'MyWorkflow#step1' };
65
94
  const next = vi.fn().mockResolvedValue( 'result' );
66
95
 
67
- memoToHeadersMock.mockReturnValue( { executionContext: { id: 'ctx-1' } } );
96
+ memoToHeadersMock.mockReturnValue( { traceInfo: workflowInfo.memo.traceInfo, workflowDetails } );
68
97
 
69
98
  const out = await interceptor.scheduleActivity( input, next );
70
99
 
71
- expect( memoToHeadersMock ).toHaveBeenCalledWith( { executionContext: { id: 'ctx-1' } } );
72
- expect( input.headers ).toEqual( { existing: 'header', executionContext: { id: 'ctx-1' } } );
100
+ expect( memoToHeadersMock ).toHaveBeenCalledWith( {
101
+ traceInfo: workflowInfo.memo.traceInfo,
102
+ workflowDetails
103
+ } );
104
+ expect( input.headers ).toEqual( {
105
+ existing: 'header',
106
+ traceInfo: workflowInfo.memo.traceInfo,
107
+ workflowDetails
108
+ } );
73
109
  expect( next ).toHaveBeenCalledWith( input );
74
110
  expect( out ).toBe( 'result' );
75
111
  } );
@@ -77,8 +113,8 @@ describe( 'workflow interceptors', () => {
77
113
  it( 'merges stepOptions with memo.activityOptions when stepOptions exist for activityType', async () => {
78
114
  stepOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
79
115
  workflowInfoMock.mockReturnValue( {
80
- workflowType: 'MyWorkflow',
81
- memo: { executionContext: {}, activityOptions: { heartbeatTimeout: 10 } }
116
+ ...workflowInfo,
117
+ memo: { traceInfo: workflowInfo.memo.traceInfo, activityOptions: { heartbeatTimeout: 10 } }
82
118
  } );
83
119
  memoToHeadersMock.mockReturnValue( {} );
84
120
  deepMergeMock.mockReturnValue( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );