@outputai/core 0.2.1-next.7e1c76d.0 → 0.2.1-next.815b3a9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.2.1-next.7e1c76d.0",
3
+ "version": "0.2.1-next.815b3a9.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,32 +1,45 @@
1
1
  /**
2
- * Record the start of an event on the default trace for the current workflow.
2
+ * Creates a new event.
3
3
  *
4
- * @param args - Event information
5
- * @param args.id - A unique id for the Event, must be the same across all phases: start, end, error.
4
+ * @param args
5
+ * @param args.id - A unique id for the Event.
6
6
  * @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
7
- * @param args.name - The human friendly name of the Event: query, request, create.
8
- * @param args.details - Arbitrary metadata associated with this phase (e.g., payloads, summaries).
7
+ * @param args.name - The human-friendly name of the Event: query, request, create.
8
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
9
9
  */
10
10
  export declare function addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
11
11
 
12
12
  /**
13
- * Record the end of an event on the default trace for the current workflow.
13
+ * Concludes an event.
14
14
  *
15
- * Use the same id as the start phase to correlate phases.
16
- *
17
- * @param args - Event information
18
- * @param args.id - Identifier matching the event's start phase.
19
- * @param args.details - Arbitrary metadata associated with this phase (e.g., results, response body).
15
+ * @param args
16
+ * @param args.id - The id of the event to conclude.
17
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
20
18
  */
21
19
  export declare function addEventEnd( args: { id: string; details: unknown } ): void;
22
20
 
23
21
  /**
24
- * Record an error for an event on the default trace for the current workflow.
25
- *
26
- * Use the same id as the start phase to correlate phases.
22
+ * Concludes an event with an error.
27
23
  *
28
- * @param args - Event metadata for the error phase.
29
- * @param args.id - Identifier matching the event's start phase.
30
- * @param args.details - Arbitrary metadata associated with this phase, possible error info.
24
+ * @param args
25
+ * @param args.id - The id of the event to conclude.
26
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
31
27
  */
32
28
  export declare function addEventError( args: { id: string; details: unknown } ): void;
29
+
30
+ /**
31
+ * Adds an attribute to an event.
32
+ *
33
+ * @param args
34
+ * @param args.eventId - The id of the event to attach the attribute to.
35
+ * @param args.name - The attribute name
36
+ * @param args.value - The attribute value
37
+ */
38
+ export declare function addEventAttribute( args: { eventId: string; name: string, value: unknown } ): void;
39
+
40
+ /**
41
+ * Known attributes.
42
+ */
43
+ export declare const Attribute: {
44
+ COST: 'cost';
45
+ };
@@ -1,37 +1,53 @@
1
- import { addEventPhaseWithContext } from '#tracing';
1
+ import { addEventActionWithContext, EventAction } from '#tracing';
2
2
 
3
3
  /**
4
- * Adds the start phase of a new event at the default trace for the current workflow.
4
+ * Creates a new event.
5
5
  *
6
6
  * @param {object} args
7
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
7
+ * @param {string} args.id - A unique id for the Event.
8
8
  * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
9
- * @param {string} args.name - The human friendly name of the Event: query, request, create.
10
- * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
9
+ * @param {string} args.name - The human-friendly name of the Event: query, request, create.
10
+ * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
11
11
  * @returns {void}
12
12
  */
13
- export const addEventStart = ( { id, kind, name, details } ) => addEventPhaseWithContext( 'start', { kind, name, details, id } );
13
+ export const addEventStart = ( { id, kind, name, details } ) =>
14
+ addEventActionWithContext( EventAction.START, { kind, name, details, id } );
14
15
 
15
16
  /**
16
- * Adds the end phase at an event at the default trace for the current workflow.
17
- *
18
- * It needs to use the same id of the start phase.
17
+ * Concludes an event.
19
18
  *
20
19
  * @param {object} args
21
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
22
- * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
20
+ * @param {string} args.id - The id of the event to conclude.
21
+ * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "output" field.
23
22
  * @returns {void}
24
23
  */
25
- export const addEventEnd = ( { id, details } ) => addEventPhaseWithContext( 'end', { id, details } );
24
+ export const addEventEnd = ( { id, details } ) => addEventActionWithContext( EventAction.END, { id, details } );
26
25
 
27
26
  /**
28
- * Adds the error phase at an event as error at the default trace for the current workflow.
27
+ * Concludes an event with an error.
29
28
  *
30
- * It needs to use the same id of the start phase.
29
+ * @param {object} args
30
+ * @param {string} args.id - The id of the event to conclude.
31
+ * @param {object} args.details - Arbitrary data to add to this event, it will be used as the "error" field.
32
+ * @returns {void}
33
+ */
34
+ export const addEventError = ( { id, details } ) => addEventActionWithContext( EventAction.ERROR, { id, details } );
35
+
36
+ /**
37
+ * Adds an attribute to an event.
31
38
  *
32
39
  * @param {object} args
33
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
34
- * @param {object} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
40
+ * @param {string} args.eventId - The id of the event to attach the attribute to.
41
+ * @param {string} args.name - The attribute name
42
+ * @param {unknown} args.value - The attribute value
35
43
  * @returns {void}
36
44
  */
37
- export const addEventError = ( { id, details } ) => addEventPhaseWithContext( 'error', { id, details } );
45
+ export const addEventAttribute = ( { eventId, name, value } ) =>
46
+ addEventActionWithContext( EventAction.ADD_ATTR, { id: eventId, details: { name, value } } );
47
+
48
+ /**
49
+ * Known attributes
50
+ */
51
+ export const Attribute = {
52
+ COST: 'cost'
53
+ };
@@ -1,4 +1,5 @@
1
- import { addEventPhase, addEventPhaseWithContext, init, getDestinations } from './trace_engine.js';
1
+ import { addEventAction, addEventActionWithContext, init, getDestinations } from './trace_engine.js';
2
+ import { EventAction } from './trace_consts.js';
2
3
 
3
4
  /**
4
5
  * Init method, if not called, no processors are attached and trace functions are dummy
@@ -6,66 +7,71 @@ import { addEventPhase, addEventPhaseWithContext, init, getDestinations } from '
6
7
  export { init, getDestinations };
7
8
 
8
9
  /**
9
- * Internal use only - adds event phase with AsyncLocalStorage context resolution
10
+ * Internal use only - adds an action with AsyncLocalStorage context resolution
10
11
  */
11
- export { addEventPhaseWithContext };
12
+ export { addEventActionWithContext };
13
+
14
+ export { EventAction };
12
15
 
13
16
  /**
14
17
  * Trace nomenclature
15
18
  *
16
19
  * Trace - The collection of Events;
17
- * Event - Any entry in the Trace file, must have the two phases START and END or ERROR;
18
- * Phase - An specific part of an Event, either START or the conclusive END or ERROR;
20
+ * Event - Any entry in the Trace file must have both START and END/ERROR actions, plus any number of ADD_ATTR actions;
21
+ * Action - A specific part of an Event: START, END, ERROR, or ADD_ATTR;
19
22
  */
20
23
 
21
24
  /**
22
25
  * Internal use only
23
26
  *
24
- * Adds the start phase of a new event at the default trace for the current workflow.
27
+ * Creates a new event and appends it to the trace.
25
28
  *
26
29
  * @param {object} args
27
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
30
+ * @param {string} args.id - A unique id for the Event.
28
31
  * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
29
- * @param {string} args.name - The human friendly name of the Event: query, request, create.
30
- * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
31
- * @param {string} args.parentId - The parent Event, used to build a three.
32
+ * @param {string} args.name - The human-friendly name of the Event: query, request, create.
33
+ * @param {any} args.details - Arbitrary data to add to this event, it will be used as the "input" field.
34
+ * @param {string} args.parentId - The parent Event, used to build a tree.
32
35
  * @param {object} args.executionContext - The original execution context from the workflow
33
36
  * @returns {void}
34
37
  */
35
- export const addEventStart = options => addEventPhase( 'start', options );
38
+ export const addEventStart = options => addEventAction( EventAction.START, options );
36
39
 
37
40
  /**
38
41
  * Internal use only
39
42
  *
40
- * Adds the end phase at an event at the default trace for the current workflow.
41
- *
42
- * It needs to use the same id of the start phase.
43
+ * Concludes an event, matching by its id.
43
44
  *
44
45
  * @param {object} args
45
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
46
- * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
47
- * @param {string} args.name - The human friendly name of the Event: query, request, create.
48
- * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
49
- * @param {string} args.parentId - The parent Event, used to build a three.
46
+ * @param {string} args.id - The id of the event to conclude.
47
+ * @param {any} args.details - Arbitrary data to add to the event; it is used as the "output" field.
50
48
  * @param {object} args.executionContext - The original execution context from the workflow
51
49
  * @returns {void}
52
50
  */
53
- export const addEventEnd = options => addEventPhase( 'end', options );
51
+ export const addEventEnd = options => addEventAction( EventAction.END, options );
54
52
 
55
53
  /**
56
54
  * Internal use only
57
55
  *
58
- * Adds the error phase at an event as error at the default trace for the current workflow.
56
+ * Concludes an event with an error, matching by its id.
57
+ *
58
+ * @param {object} args
59
+ * @param {string} args.id - The id of the event to conclude.
60
+ * @param {any} args.details - Arbitrary data to add to the event; it is used as the "error" field.
61
+ * @param {object} args.executionContext - The original execution context from the workflow
62
+ * @returns {void}
63
+ */
64
+ export const addEventError = options => addEventAction( EventAction.ERROR, options );
65
+
66
+ /**
67
+ * Internal use only
59
68
  *
60
- * It needs to use the same id of the start phase.
69
+ * Adds an attribute to an event using its id.
61
70
  *
62
71
  * @param {object} args
63
- * @param {string} args.id - A unique id for the Event, must be the same across all phases: start, end, error.
64
- * @param {string} args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
65
- * @param {string} args.name - The human friendly name of the Event: query, request, create.
66
- * @param {any} args.details - All details attached to this Event Phase. DB queried records, HTTP response body.
67
- * @param {string} args.parentId - The parent Event, used to build a three.
72
+ * @param {string} args.id - The id of the event to attach the attribute to.
73
+ * @param {object} args.details - The attribute to add to this event, must be in `{ name: string, value: any }` format.
68
74
  * @param {object} args.executionContext - The original execution context from the workflow
69
75
  * @returns {void}
70
76
  */
71
- export const addEventError = options => addEventPhase( 'error', options );
77
+ export const addEventAttribute = options => addEventAction( EventAction.ADD_ATTR, options );
@@ -23,7 +23,7 @@ const tempTraceFilesDir = join( __dirname, 'temp', 'traces' );
23
23
  const createTempFilePath = ( { workflowId, startTime } ) => join( tempTraceFilesDir, `${startTime}_${workflowId}.trace` );
24
24
 
25
25
  /**
26
- * Adds an trace entry to the accumulation file
26
+ * Adds a trace entry to the accumulation file.
27
27
  * @param {object} entry - The trace entry
28
28
  * @param {string} path - Accumulation file path
29
29
  */
@@ -100,12 +100,13 @@ export const init = () => {
100
100
  /**
101
101
  * Execute this processor:
102
102
  *
103
- * Append each trace entry to a temp file; when the root workflow ends (non-start phase on the
104
- * workflow id) or any entry is an error phase, build the trace tree and write the JSON file once.
103
+ * Appends each trace entry to a temp file.
104
+ *
105
+ * When the root workflow ends or the entry is an error action, build the trace tree and write the JSON file.
105
106
  *
106
107
  * @param {object} args
107
- * @param {object} entry - Trace event phase
108
- * @param {object} executionContext - Execution info: workflowId, workflowName, startTime
108
+ * @param {object} args.entry - The trace entry to append.
109
+ * @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
109
110
  * @returns {void}
110
111
  */
111
112
  export const exec = ( { entry, executionContext } ) => {
@@ -113,8 +114,8 @@ export const exec = ( { entry, executionContext } ) => {
113
114
  const tempFilePath = createTempFilePath( executionContext );
114
115
  addEntry( entry, tempFilePath );
115
116
 
116
- const isRootWorkflowEnd = entry.id === workflowId && entry.phase !== 'start';
117
- const isError = entry.phase === 'error';
117
+ const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
118
+ const isError = entry.action === 'error';
118
119
 
119
120
  if ( !isRootWorkflowEnd && !isError ) {
120
121
  return;
@@ -25,10 +25,10 @@ vi.mock( 'node:fs', () => ( {
25
25
  const buildTraceTreeMock = vi.fn( entries => ( { count: entries.length } ) );
26
26
  vi.mock( '../../tools/build_trace_tree.js', () => ( { default: buildTraceTreeMock } ) );
27
27
 
28
- /** Flush happens when root id matches workflowId and phase is not start, or when phase is error. */
29
- const rootStart = ( workflowId, ts ) => ( { id: workflowId, phase: 'start', timestamp: ts } );
30
- const rootEnd = ( workflowId, ts ) => ( { id: workflowId, phase: 'end', timestamp: ts } );
31
- const childTick = ( id, ts ) => ( { id, phase: 'tick', timestamp: ts } );
28
+ /** Flush happens when the root id matches workflowId and action is not 'start', or when action is 'error'. */
29
+ const rootStart = ( workflowId, ts ) => ( { id: workflowId, action: 'start', timestamp: ts } );
30
+ const rootEnd = ( workflowId, ts ) => ( { id: workflowId, action: 'end', timestamp: ts } );
31
+ const childTick = ( id, ts ) => ( { id, action: 'tick', timestamp: ts } );
32
32
 
33
33
  describe( 'tracing/processors/local', () => {
34
34
  beforeEach( () => {
@@ -86,7 +86,7 @@ describe( 'tracing/processors/local', () => {
86
86
  expect( writeFileSyncMock ).not.toHaveBeenCalled();
87
87
  } );
88
88
 
89
- it( 'exec(): flushes on error phase before root end', async () => {
89
+ it( 'exec(): flushes on error action before root end', async () => {
90
90
  const { exec, init } = await import( './index.js' );
91
91
  init();
92
92
 
@@ -95,7 +95,7 @@ describe( 'tracing/processors/local', () => {
95
95
  const ctx = { executionContext: { workflowId, workflowName: 'WF', startTime } };
96
96
 
97
97
  exec( { ...ctx, entry: rootStart( workflowId, startTime ) } );
98
- exec( { ...ctx, entry: { id: 'step-1', phase: 'error', timestamp: startTime + 1 } } );
98
+ exec( { ...ctx, entry: { id: 'step-1', action: 'error', timestamp: startTime + 1 } } );
99
99
 
100
100
  expect( buildTraceTreeMock ).toHaveBeenCalledTimes( 1 );
101
101
  expect( buildTraceTreeMock.mock.calls[0][0] ).toHaveLength( 2 );
@@ -67,11 +67,15 @@ export const init = async () => {
67
67
  };
68
68
 
69
69
  /**
70
- * Execute this processor: send a complete trace tree file to S3 when the workflow finishes
70
+ * Execute this processor:
71
+ *
72
+ * Appends each trace entry to Redis.
73
+ *
74
+ * When the root workflow ends or the entry is an error action, it builds the trace tree and uploads it to S3.
71
75
  *
72
76
  * @param {object} args
73
- * @param {object} entry - Trace event phase
74
- * @param {object} executionContext - Execution info: workflowId, workflowName, startTime
77
+ * @param {object} args.entry - The trace entry to append
78
+ * @param {object} args.executionContext - Execution info: workflowId, workflowName, startTime
75
79
  */
76
80
  export const exec = async ( { entry, executionContext } ) => {
77
81
  const { workflowName, workflowId, startTime } = executionContext;
@@ -79,7 +83,7 @@ export const exec = async ( { entry, executionContext } ) => {
79
83
 
80
84
  await addEntry( entry, cacheKey );
81
85
 
82
- const isRootWorkflowEnd = entry.id === workflowId && entry.phase !== 'start';
86
+ const isRootWorkflowEnd = entry.id === workflowId && entry.action !== 'start';
83
87
  if ( !isRootWorkflowEnd ) {
84
88
  return;
85
89
  }
@@ -52,9 +52,9 @@ describe( 'tracing/processors/s3', () => {
52
52
 
53
53
  redisMulti.exec.mockResolvedValue( [] );
54
54
 
55
- const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', phase: 'start', details: {}, timestamp: startTime };
56
- const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1', phase: 'start', details: {}, timestamp: startTime + 1 };
57
- const workflowEnd = { id: 'id1', phase: 'end', details: { ok: true }, timestamp: startTime + 2 };
55
+ const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
56
+ const activityStart = { id: 'act-1', name: 'DoSomething', kind: 'step', parentId: 'id1', action: 'start', details: {}, timestamp: startTime + 1 };
57
+ const workflowEnd = { id: 'id1', action: 'end', details: { ok: true }, timestamp: startTime + 2 };
58
58
  zRangeMock.mockResolvedValue( [
59
59
  JSON.stringify( workflowStart ),
60
60
  JSON.stringify( activityStart ),
@@ -97,7 +97,7 @@ describe( 'tracing/processors/s3', () => {
97
97
 
98
98
  redisMulti.exec.mockResolvedValue( [] );
99
99
  const workflowStart = {
100
- kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, phase: 'start', details: {}, timestamp: startTime
100
+ kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'start', details: {}, timestamp: startTime
101
101
  };
102
102
  zRangeMock.mockResolvedValue( [ JSON.stringify( workflowStart ) ] );
103
103
 
@@ -113,8 +113,8 @@ describe( 'tracing/processors/s3', () => {
113
113
  const ctx = { executionContext: { workflowId: 'id1', workflowName: 'WF', startTime } };
114
114
 
115
115
  redisMulti.exec.mockResolvedValue( [] );
116
- const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', phase: 'start', details: {}, timestamp: startTime };
117
- const stepEndNoParent = { id: 'step-1', phase: 'end', details: { done: true }, timestamp: startTime + 1 };
116
+ const workflowStart = { id: 'id1', name: 'WF', kind: 'workflow', action: 'start', details: {}, timestamp: startTime };
117
+ const stepEndNoParent = { id: 'step-1', action: 'end', details: { done: true }, timestamp: startTime + 1 };
118
118
  zRangeMock.mockResolvedValue( [
119
119
  JSON.stringify( workflowStart ),
120
120
  JSON.stringify( stepEndNoParent )
@@ -136,7 +136,7 @@ describe( 'tracing/processors/s3', () => {
136
136
 
137
137
  redisMulti.exec.mockResolvedValue( [] );
138
138
  const workflowEnd = {
139
- kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, phase: 'end', details: {}, timestamp: startTime
139
+ kind: 'workflow', id: 'id1', name: 'WF', parentId: undefined, action: 'end', details: {}, timestamp: startTime
140
140
  };
141
141
  zRangeMock.mockResolvedValue( [ JSON.stringify( workflowEnd ) ] );
142
142
  buildTraceTreeMock.mockReturnValueOnce( null );
@@ -1,3 +1,4 @@
1
+ import { EventAction } from '../trace_consts.js';
1
2
  /**
2
3
  * @typedef {object} NodeEntry
3
4
  * @property {string} id
@@ -27,19 +28,21 @@ const createEntry = id => ( {
27
28
  input: undefined,
28
29
  output: undefined,
29
30
  error: undefined,
30
- children: []
31
+ children: [],
32
+ attributes: {}
31
33
  } );
32
34
 
33
35
  /**
34
- * Build a tree of nodes from a list of entries
36
+ * Builds a tree of nodes from a list of entries.
35
37
  *
36
38
  * Each node will have: id, name, kind, children, input, output or error, startedAt, endedAt.
37
39
  *
38
- * Entries with same id will be combined according to their phase (start, end OR error).
39
- * - The details of the start phase becomes input, timestamp becomes startedAt;
40
- * - The details of the end phase become output, timestamp becomes endedAt;
41
- * - The details of the error phase become error, timestamp becomes endedAt;
42
- * - Only start phase's kind and name are used;
40
+ * Entries with the same id are combined according to their actions.
41
+ * - The details of the START action become input, and timestamp becomes startedAt;
42
+ * - The details of the END action become output, timestamp becomes endedAt;
43
+ * - The details of the ERROR action become error, timestamp becomes endedAt;
44
+ * - The details of the ADD_ATTR action are attached to `.attributes`;
45
+ * - Only the START action's `kind` and `name` fields are used;
43
46
  *
44
47
  *
45
48
  * Children are added according to the parentId of each entry.
@@ -53,18 +56,20 @@ export default entries => {
53
56
  const ensureNode = id => nodes.get( id ) ?? nodes.set( id, createEntry( id ) ).get( id );
54
57
 
55
58
  for ( const entry of entries ) {
56
- const { kind, id, name, parentId, details, phase, timestamp } = entry;
59
+ const { kind, id, name, parentId, details, action, timestamp } = entry;
57
60
  const node = ensureNode( id );
58
61
 
59
- if ( phase === 'start' ) {
62
+ if ( action === EventAction.START ) {
60
63
  Object.assign( node, { input: details, startedAt: timestamp, kind, name } );
61
- } else if ( phase === 'end' ) {
64
+ } else if ( action === EventAction.ADD_ATTR ) {
65
+ node.attributes[details.name] = details.value;
66
+ } else if ( action === EventAction.END ) {
62
67
  Object.assign( node, { output: details, endedAt: timestamp } );
63
- } else if ( phase === 'error' ) {
68
+ } else if ( action === EventAction.ERROR ) {
64
69
  Object.assign( node, { error: details, endedAt: timestamp } );
65
70
  }
66
71
 
67
- if ( parentId && phase === 'start' ) {
72
+ if ( parentId && action === EventAction.START ) {
68
73
  const parent = ensureNode( parentId );
69
74
  parent.children.push( node );
70
75
  parent.children.sort( ( a, b ) => a.startedAt - b.startedAt );
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
+ import { EventAction } from '../trace_consts.js';
2
3
  import buildTraceTree from './build_trace_tree.js';
3
4
 
4
5
  describe( 'build_trace_tree', () => {
@@ -6,9 +7,9 @@ describe( 'build_trace_tree', () => {
6
7
  expect( buildTraceTree( [] ) ).toBeNull();
7
8
  } );
8
9
 
9
- it( 'sets root output with a fixed message when workflow has no end/error phase yet', () => {
10
+ it( 'sets root output with a fixed message when workflow has no end/error action yet', () => {
10
11
  const entries = [
11
- { kind: 'workflow', id: 'wf', parentId: undefined, phase: 'start', name: 'wf', details: {}, timestamp: 1000 }
12
+ { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 1000 }
12
13
  ];
13
14
  const result = buildTraceTree( entries );
14
15
  expect( result ).not.toBeNull();
@@ -19,17 +20,52 @@ this can indicate it timed out or was interrupted.>>' );
19
20
 
20
21
  it( 'returns null when there is no root (all entries have parentId)', () => {
21
22
  const entries = [
22
- { id: 'a', parentId: 'x', phase: 'start', name: 'a', timestamp: 1 },
23
- { id: 'b', parentId: 'a', phase: 'start', name: 'b', timestamp: 2 }
23
+ { id: 'a', parentId: 'x', action: EventAction.START, name: 'a', timestamp: 1 },
24
+ { id: 'b', parentId: 'a', action: EventAction.START, name: 'b', timestamp: 2 }
24
25
  ];
25
26
  expect( buildTraceTree( entries ) ).toBeNull();
26
27
  } );
27
28
 
28
- it( 'error phase sets error and endedAt on node', () => {
29
+ it( 'add_attr action merges details.name and details.value into node.attributes', () => {
29
30
  const entries = [
30
- { kind: 'wf', id: 'r', parentId: undefined, phase: 'start', name: 'root', details: {}, timestamp: 100 },
31
- { kind: 'step', id: 's', parentId: 'r', phase: 'start', name: 'step', details: {}, timestamp: 200 },
32
- { id: 's', phase: 'error', details: { message: 'failed' }, timestamp: 300 }
31
+ { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 100 },
32
+ { kind: 'step', id: 's', parentId: 'wf', action: EventAction.START, name: 'step', details: {}, timestamp: 200 },
33
+ { id: 's', action: EventAction.ADD_ATTR, details: { name: 'latency_ms', value: 42 }, timestamp: 250 },
34
+ { id: 's', action: EventAction.ADD_ATTR, details: { name: 'retries', value: 1 }, timestamp: 260 },
35
+ { id: 'wf', action: EventAction.END, details: {}, timestamp: 300 }
36
+ ];
37
+ const result = buildTraceTree( entries );
38
+ expect( result ).not.toBeNull();
39
+ expect( result.children[0].attributes ).toEqual( { latency_ms: 42, retries: 1 } );
40
+ } );
41
+
42
+ it( 'add_attr action overwrites prior value for the same attribute name', () => {
43
+ const entries = [
44
+ { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 1 },
45
+ { id: 'wf', action: EventAction.ADD_ATTR, details: { name: 'x', value: 1 }, timestamp: 2 },
46
+ { id: 'wf', action: EventAction.ADD_ATTR, details: { name: 'x', value: 2 }, timestamp: 3 },
47
+ { id: 'wf', action: EventAction.END, details: {}, timestamp: 4 }
48
+ ];
49
+ const result = buildTraceTree( entries );
50
+ expect( result.attributes ).toEqual( { x: 2 } );
51
+ } );
52
+
53
+ it( 'add_attr does not attach nodes as children (only start does)', () => {
54
+ const entries = [
55
+ { kind: 'workflow', id: 'wf', parentId: undefined, action: EventAction.START, name: 'wf', details: {}, timestamp: 1 },
56
+ { id: 'orphan', parentId: 'wf', action: EventAction.ADD_ATTR, details: { name: 'k', value: 'v' }, timestamp: 2 },
57
+ { id: 'wf', action: EventAction.END, details: {}, timestamp: 3 }
58
+ ];
59
+ const result = buildTraceTree( entries );
60
+ expect( result.children ).toHaveLength( 0 );
61
+ expect( result.attributes ).toEqual( {} );
62
+ } );
63
+
64
+ it( 'error action sets error and endedAt on node', () => {
65
+ const entries = [
66
+ { kind: 'wf', id: 'r', parentId: undefined, action: EventAction.START, name: 'root', details: {}, timestamp: 100 },
67
+ { kind: 'step', id: 's', parentId: 'r', action: EventAction.START, name: 'step', details: {}, timestamp: 200 },
68
+ { id: 's', action: EventAction.ERROR, details: { message: 'failed' }, timestamp: 300 }
33
69
  ];
34
70
  const result = buildTraceTree( entries );
35
71
  expect( result ).not.toBeNull();
@@ -41,27 +77,28 @@ this can indicate it timed out or was interrupted.>>' );
41
77
  it( 'builds a tree from workflow/step/IO entries with grouping and sorting', () => {
42
78
  const entries = [
43
79
  // workflow start
44
- { kind: 'workflow', phase: 'start', name: 'wf', id: 'wf', parentId: undefined, details: { a: 1 }, timestamp: 1000 },
80
+ { kind: 'workflow', action: EventAction.START, name: 'wf', id: 'wf', parentId: undefined, details: { a: 1 }, timestamp: 1000 },
45
81
  // evaluator start/stop
46
- { kind: 'evaluator', phase: 'start', name: 'eval', id: 'eval', parentId: 'wf', details: { z: 0 }, timestamp: 1500 },
47
- { id: 'eval', phase: 'end', details: { z: 1 }, timestamp: 1600 },
82
+ { kind: 'evaluator', action: EventAction.START, name: 'eval', id: 'eval', parentId: 'wf', details: { z: 0 }, timestamp: 1500 },
83
+ { id: 'eval', action: EventAction.END, details: { z: 1 }, timestamp: 1600 },
48
84
  // step1 start
49
- { kind: 'step', phase: 'start', name: 'step-1', id: 's1', parentId: 'wf', details: { x: 1 }, timestamp: 2000 },
85
+ { kind: 'step', action: EventAction.START, name: 'step-1', id: 's1', parentId: 'wf', details: { x: 1 }, timestamp: 2000 },
86
+ { id: 's1', action: EventAction.ADD_ATTR, details: { name: 'step_tag', value: 'alpha' }, timestamp: 2050 },
50
87
  // IO under step1
51
- { kind: 'IO', phase: 'start', name: 'test-1', id: 'io1', parentId: 's1', details: { y: 2 }, timestamp: 2300 },
88
+ { kind: 'IO', action: EventAction.START, name: 'test-1', id: 'io1', parentId: 's1', details: { y: 2 }, timestamp: 2300 },
52
89
  // step2 start
53
- { kind: 'step', phase: 'start', name: 'step-2', id: 's2', parentId: 'wf', details: { x: 2 }, timestamp: 2400 },
90
+ { kind: 'step', action: EventAction.START, name: 'step-2', id: 's2', parentId: 'wf', details: { x: 2 }, timestamp: 2400 },
54
91
  // IO under step2
55
- { kind: 'IO', phase: 'start', name: 'test-2', id: 'io2', parentId: 's2', details: { y: 3 }, timestamp: 2500 },
56
- { id: 'io2', phase: 'end', details: { y: 4 }, timestamp: 2600 },
92
+ { kind: 'IO', action: EventAction.START, name: 'test-2', id: 'io2', parentId: 's2', details: { y: 3 }, timestamp: 2500 },
93
+ { id: 'io2', action: EventAction.END, details: { y: 4 }, timestamp: 2600 },
57
94
  // IO under step1 ends
58
- { id: 'io1', phase: 'end', details: { y: 5 }, timestamp: 2700 },
95
+ { id: 'io1', action: EventAction.END, details: { y: 5 }, timestamp: 2700 },
59
96
  // step1 end
60
- { id: 's1', phase: 'end', details: { done: true }, timestamp: 2800 },
97
+ { id: 's1', action: EventAction.END, details: { done: true }, timestamp: 2800 },
61
98
  // step2 end
62
- { id: 's2', phase: 'end', details: { done: true }, timestamp: 2900 },
99
+ { id: 's2', action: EventAction.END, details: { done: true }, timestamp: 2900 },
63
100
  // workflow end
64
- { id: 'wf', phase: 'end', details: { ok: true }, timestamp: 3000 }
101
+ { id: 'wf', action: EventAction.END, details: { ok: true }, timestamp: 3000 }
65
102
  ];
66
103
 
67
104
  const result = buildTraceTree( entries );
@@ -74,6 +111,7 @@ this can indicate it timed out or was interrupted.>>' );
74
111
  endedAt: 3000,
75
112
  input: { a: 1 },
76
113
  output: { ok: true },
114
+ attributes: {},
77
115
  children: [
78
116
  {
79
117
  id: 'eval',
@@ -83,6 +121,7 @@ this can indicate it timed out or was interrupted.>>' );
83
121
  endedAt: 1600,
84
122
  input: { z: 0 },
85
123
  output: { z: 1 },
124
+ attributes: {},
86
125
  children: []
87
126
  },
88
127
  {
@@ -93,6 +132,7 @@ this can indicate it timed out or was interrupted.>>' );
93
132
  endedAt: 2800,
94
133
  input: { x: 1 },
95
134
  output: { done: true },
135
+ attributes: { step_tag: 'alpha' },
96
136
  children: [
97
137
  {
98
138
  id: 'io1',
@@ -102,6 +142,7 @@ this can indicate it timed out or was interrupted.>>' );
102
142
  endedAt: 2700,
103
143
  input: { y: 2 },
104
144
  output: { y: 5 },
145
+ attributes: {},
105
146
  children: []
106
147
  }
107
148
  ]
@@ -114,6 +155,7 @@ this can indicate it timed out or was interrupted.>>' );
114
155
  endedAt: 2900,
115
156
  input: { x: 2 },
116
157
  output: { done: true },
158
+ attributes: {},
117
159
  children: [
118
160
  {
119
161
  id: 'io2',
@@ -123,6 +165,7 @@ this can indicate it timed out or was interrupted.>>' );
123
165
  endedAt: 2600,
124
166
  input: { y: 3 },
125
167
  output: { y: 4 },
168
+ attributes: {},
126
169
  children: []
127
170
  }
128
171
  ]
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Each possible action of a trace event
3
+ */
4
+ export const EventAction = {
5
+ START: 'start',
6
+ END: 'end',
7
+ ERROR: 'error',
8
+ ADD_ATTR: 'add_attr'
9
+ };
@@ -64,34 +64,34 @@ export const init = async () => {
64
64
  const serializeDetails = details => details instanceof Error ? serializeError( details ) : details;
65
65
 
66
66
  /**
67
- * Creates a new trace event phase and sends it to be written
67
+ * Emits an event action to the event bus.
68
68
  *
69
- * @param {string} phase - The phase
69
+ * @param {string} action - The action
70
70
  * @param {object} fields - All the trace fields
71
71
  * @returns {void}
72
72
  */
73
- export const addEventPhase = ( phase, { kind, name, id, parentId, details, executionContext } ) => {
73
+ export const addEventAction = ( action, { kind, name, id, parentId, details, executionContext } ) => {
74
74
  // Ignores internal steps in the actual trace files, ignore trace if the flag is true
75
75
  if ( kind !== ComponentType.INTERNAL_STEP && !executionContext.disableTrace ) {
76
76
  traceBus.emit( 'entry', {
77
77
  executionContext,
78
- entry: { kind, phase, name, id, parentId, timestamp: Date.now(), details: serializeDetails( details ) }
78
+ entry: { kind, action, name, id, parentId, timestamp: Date.now(), details: serializeDetails( details ) }
79
79
  } );
80
80
  }
81
81
  };
82
82
 
83
83
  /**
84
- * Adds an Event Phase, complementing the options with parentId and executionContext from the async storage.
84
+ * Attaches contextual information to an event action before calling the method to emit it to the bus.
85
85
  *
86
- * This function will have no effect if called from outside an Temporal Workflow/Activity environment,
87
- * so it is safe to be used on unit tests or any dependencies that might be used elsewhere
86
+ * This function has no effect if called outside a Temporal Workflow/Activity environment,
87
+ * so it is safe to use in unit tests or dependencies that might be used elsewhere.
88
88
  *
89
89
  * @param {object} options - The common trace configurations
90
90
  */
91
- export function addEventPhaseWithContext( phase, options ) {
91
+ export function addEventActionWithContext( action, options ) {
92
92
  const storeContent = Storage.load();
93
- if ( storeContent ) { // If there is no storageContext this was not called from an Temporal Environment
93
+ if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
94
94
  const { parentId, executionContext } = storeContent;
95
- addEventPhase( phase, { ...options, parentId, executionContext } );
95
+ addEventAction( action, { ...options, parentId, executionContext } );
96
96
  }
97
97
  };
@@ -39,7 +39,7 @@ describe( 'tracing/trace_engine', () => {
39
39
  it( 'init() starts only enabled processors and attaches listeners', async () => {
40
40
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
41
41
  process.env.OUTPUT_TRACE_REMOTE_ON = '0';
42
- const { init, addEventPhase } = await loadTraceEngine();
42
+ const { init, addEventAction } = await loadTraceEngine();
43
43
 
44
44
  await init();
45
45
 
@@ -47,96 +47,96 @@ describe( 'tracing/trace_engine', () => {
47
47
  expect( s3InitMock ).not.toHaveBeenCalled();
48
48
 
49
49
  const executionContext = { disableTrace: false };
50
- addEventPhase( 'start', {
50
+ addEventAction( 'start', {
51
51
  kind: 'step', name: 'N', id: '1', parentId: 'p', details: { ok: true }, executionContext
52
52
  } );
53
53
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
54
54
  const payload = localExecMock.mock.calls[0][0];
55
55
  expect( payload.entry.name ).toBe( 'N' );
56
56
  expect( payload.entry.kind ).toBe( 'step' );
57
- expect( payload.entry.phase ).toBe( 'start' );
57
+ expect( payload.entry.action ).toBe( 'start' );
58
58
  expect( payload.entry.details ).toEqual( { ok: true } );
59
59
  expect( payload.executionContext ).toBe( executionContext );
60
60
  } );
61
61
 
62
- it( 'addEventPhase() emits an entry consumed by processors', async () => {
62
+ it( 'addEventAction() emits an entry consumed by processors', async () => {
63
63
  process.env.OUTPUT_TRACE_LOCAL_ON = 'on';
64
- const { init, addEventPhase } = await loadTraceEngine();
64
+ const { init, addEventAction } = await loadTraceEngine();
65
65
  await init();
66
66
 
67
- addEventPhase( 'end', {
67
+ addEventAction( 'end', {
68
68
  kind: 'workflow', name: 'W', id: '2', parentId: 'p2', details: 'done',
69
69
  executionContext: { disableTrace: false }
70
70
  } );
71
71
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
72
72
  const payload = localExecMock.mock.calls[0][0];
73
73
  expect( payload.entry.name ).toBe( 'W' );
74
- expect( payload.entry.phase ).toBe( 'end' );
74
+ expect( payload.entry.action ).toBe( 'end' );
75
75
  expect( payload.entry.details ).toBe( 'done' );
76
76
  } );
77
77
 
78
- it( 'addEventPhase() does not emit when executionContext.disableTrace is true', async () => {
78
+ it( 'addEventAction() does not emit when executionContext.disableTrace is true', async () => {
79
79
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
80
- const { init, addEventPhase } = await loadTraceEngine();
80
+ const { init, addEventAction } = await loadTraceEngine();
81
81
  await init();
82
82
 
83
- addEventPhase( 'start', {
83
+ addEventAction( 'start', {
84
84
  kind: 'step', name: 'X', id: '1', parentId: 'p', details: {},
85
85
  executionContext: { disableTrace: true }
86
86
  } );
87
87
  expect( localExecMock ).not.toHaveBeenCalled();
88
88
  } );
89
89
 
90
- it( 'addEventPhase() does not emit when kind is INTERNAL_STEP', async () => {
90
+ it( 'addEventAction() does not emit when kind is INTERNAL_STEP', async () => {
91
91
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
92
- const { init, addEventPhase } = await loadTraceEngine();
92
+ const { init, addEventAction } = await loadTraceEngine();
93
93
  await init();
94
94
 
95
- addEventPhase( 'start', {
95
+ addEventAction( 'start', {
96
96
  kind: 'internal_step', name: 'Internal', id: '1', parentId: 'p', details: {},
97
97
  executionContext: { disableTrace: false }
98
98
  } );
99
99
  expect( localExecMock ).not.toHaveBeenCalled();
100
100
  } );
101
101
 
102
- it( 'addEventPhaseWithContext() uses storage when available', async () => {
102
+ it( 'addEventActionWithContext() uses storage when available', async () => {
103
103
  process.env.OUTPUT_TRACE_LOCAL_ON = 'true';
104
104
  storageLoadMock.mockReturnValue( {
105
105
  parentId: 'ctx-p',
106
106
  executionContext: { runId: 'r1', disableTrace: false }
107
107
  } );
108
- const { init, addEventPhaseWithContext } = await loadTraceEngine();
108
+ const { init, addEventActionWithContext } = await loadTraceEngine();
109
109
  await init();
110
110
 
111
- addEventPhaseWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
111
+ addEventActionWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
112
112
  expect( localExecMock ).toHaveBeenCalledTimes( 1 );
113
113
  const payload = localExecMock.mock.calls[0][0];
114
114
  expect( payload.executionContext ).toEqual( { runId: 'r1', disableTrace: false } );
115
115
  expect( payload.entry.parentId ).toBe( 'ctx-p' );
116
116
  expect( payload.entry.name ).toBe( 'S' );
117
- expect( payload.entry.phase ).toBe( 'tick' );
117
+ expect( payload.entry.action ).toBe( 'tick' );
118
118
  } );
119
119
 
120
- it( 'addEventPhaseWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
120
+ it( 'addEventActionWithContext() does not emit when storage executionContext.disableTrace is true', async () => {
121
121
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
122
122
  storageLoadMock.mockReturnValue( {
123
123
  parentId: 'ctx-p',
124
124
  executionContext: { runId: 'r1', disableTrace: true }
125
125
  } );
126
- const { init, addEventPhaseWithContext } = await loadTraceEngine();
126
+ const { init, addEventActionWithContext } = await loadTraceEngine();
127
127
  await init();
128
128
 
129
- addEventPhaseWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
129
+ addEventActionWithContext( 'tick', { kind: 'step', name: 'S', id: '3', details: 1 } );
130
130
  expect( localExecMock ).not.toHaveBeenCalled();
131
131
  } );
132
132
 
133
- it( 'addEventPhaseWithContext() is a no-op when storage is absent', async () => {
133
+ it( 'addEventActionWithContext() is a no-op when storage is absent', async () => {
134
134
  process.env.OUTPUT_TRACE_LOCAL_ON = '1';
135
135
  storageLoadMock.mockReturnValue( undefined );
136
- const { init, addEventPhaseWithContext } = await loadTraceEngine();
136
+ const { init, addEventActionWithContext } = await loadTraceEngine();
137
137
  await init();
138
138
 
139
- addEventPhaseWithContext( 'noop', { kind: 'step', name: 'X', id: '4', details: null } );
139
+ addEventActionWithContext( 'noop', { kind: 'step', name: 'X', id: '4', details: null } );
140
140
  expect( localExecMock ).not.toHaveBeenCalled();
141
141
  } );
142
142