@outputai/core 0.8.2-next.42a0ddf.0 → 0.8.2-next.57bc8d6.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 (106) hide show
  1. package/package.json +11 -11
  2. package/src/bus.js +1 -1
  3. package/src/consts.js +5 -2
  4. package/src/helpers/component.js +12 -0
  5. package/src/helpers/component.spec.js +54 -0
  6. package/src/helpers/fetch.js +105 -0
  7. package/src/helpers/fetch.spec.js +203 -0
  8. package/src/helpers/function.js +15 -0
  9. package/src/helpers/function.spec.js +48 -0
  10. package/src/helpers/object.js +98 -0
  11. package/src/helpers/object.spec.js +377 -0
  12. package/src/helpers/promise.js +29 -0
  13. package/src/helpers/promise.spec.js +35 -0
  14. package/src/helpers/string.js +30 -0
  15. package/src/helpers/string.spec.js +64 -0
  16. package/src/hooks/index.d.ts +102 -30
  17. package/src/hooks/index.js +16 -1
  18. package/src/hooks/index.spec.js +55 -1
  19. package/src/index.d.ts +2 -2
  20. package/src/interface/evaluator.d.ts +2 -2
  21. package/src/interface/evaluator.js +14 -12
  22. package/src/interface/evaluator.spec.js +10 -6
  23. package/src/interface/index.d.ts +7 -6
  24. package/src/interface/index.js +2 -0
  25. package/src/interface/logger.d.ts +61 -0
  26. package/src/interface/logger.js +73 -0
  27. package/src/interface/logger.spec.js +172 -0
  28. package/src/interface/step.d.ts +1 -1
  29. package/src/interface/step.js +15 -12
  30. package/src/interface/step.spec.js +10 -6
  31. package/src/interface/webhook.d.ts +21 -2
  32. package/src/interface/workflow.d.ts +2 -2
  33. package/src/interface/workflow.js +85 -78
  34. package/src/interface/workflow.spec.js +11 -4
  35. package/src/internal_activities/index.js +38 -35
  36. package/src/internal_activities/index.spec.js +27 -4
  37. package/src/logger/development.js +2 -2
  38. package/src/logger/development.spec.js +19 -2
  39. package/src/logger/production.js +1 -1
  40. package/src/logger/production.spec.js +24 -5
  41. package/src/sdk/README.md +47 -0
  42. package/src/sdk/helpers/component_metadata.d.ts +17 -0
  43. package/src/sdk/helpers/component_metadata.js +6 -0
  44. package/src/sdk/helpers/component_metadata.spec.js +30 -0
  45. package/src/sdk/helpers/index.d.ts +12 -0
  46. package/src/sdk/helpers/index.js +3 -0
  47. package/src/sdk/helpers/objects.d.ts +51 -0
  48. package/src/sdk/helpers/objects.js +8 -0
  49. package/src/sdk/helpers/objects.spec.js +16 -0
  50. package/src/sdk/helpers/path.d.ts +11 -0
  51. package/src/sdk/helpers/path.js +32 -0
  52. package/src/{utils/resolve_invocation_dir.spec.js → sdk/helpers/path.spec.js} +9 -9
  53. package/src/sdk/runtime/context.d.ts +30 -0
  54. package/src/sdk/runtime/context.js +15 -0
  55. package/src/{activity_integration → sdk/runtime}/context.spec.js +5 -5
  56. package/src/sdk/runtime/events.d.ts +15 -0
  57. package/src/sdk/runtime/events.js +18 -0
  58. package/src/{activity_integration → sdk/runtime}/events.spec.js +8 -9
  59. package/src/sdk/runtime/index.d.ts +12 -0
  60. package/src/sdk/runtime/index.js +3 -0
  61. package/src/sdk/runtime/tracing.d.ts +46 -0
  62. package/src/sdk/runtime/tracing.js +11 -0
  63. package/src/tracing/processors/s3/redis_client.spec.js +0 -6
  64. package/src/tracing/processors/s3/s3_client.spec.js +0 -6
  65. package/src/tracing/trace_engine.js +1 -1
  66. package/src/worker/catalog_workflow/catalog_job.js +1 -1
  67. package/src/worker/catalog_workflow/index.spec.js +8 -11
  68. package/src/worker/configs.js +1 -1
  69. package/src/worker/connection_monitor.js +1 -1
  70. package/src/worker/global_functions.js +14 -0
  71. package/src/worker/global_functions.spec.js +55 -0
  72. package/src/worker/index.js +4 -1
  73. package/src/worker/index.spec.js +7 -0
  74. package/src/worker/interceptors/activity.js +8 -11
  75. package/src/worker/interceptors/activity.spec.js +25 -26
  76. package/src/worker/interceptors/workflow.js +3 -3
  77. package/src/worker/interceptors/workflow.spec.js +1 -1
  78. package/src/worker/loader/matchers.js +1 -1
  79. package/src/worker/log_hooks.js +14 -0
  80. package/src/worker/log_hooks.spec.js +83 -2
  81. package/src/worker/sinks.js +7 -1
  82. package/src/worker/sinks.spec.js +203 -0
  83. package/src/activity_integration/context.d.ts +0 -23
  84. package/src/activity_integration/context.js +0 -18
  85. package/src/activity_integration/event_id_integration.spec.js +0 -52
  86. package/src/activity_integration/events.d.ts +0 -10
  87. package/src/activity_integration/events.js +0 -15
  88. package/src/activity_integration/index.d.ts +0 -9
  89. package/src/activity_integration/index.js +0 -3
  90. package/src/activity_integration/tracing.d.ts +0 -40
  91. package/src/activity_integration/tracing.js +0 -48
  92. package/src/utils/index.d.ts +0 -180
  93. package/src/utils/index.js +0 -2
  94. package/src/utils/resolve_invocation_dir.js +0 -34
  95. package/src/utils/utils.js +0 -334
  96. package/src/utils/utils.spec.js +0 -723
  97. /package/src/{internal_utils → helpers}/aggregations.js +0 -0
  98. /package/src/{internal_utils → helpers}/aggregations.spec.js +0 -0
  99. /package/src/{internal_utils → helpers}/errors.js +0 -0
  100. /package/src/{internal_utils → helpers}/errors.spec.js +0 -0
  101. /package/src/{internal_utils → helpers}/temporal_context.js +0 -0
  102. /package/src/{internal_utils → helpers}/temporal_context.spec.ts +0 -0
  103. /package/src/{internal_utils → helpers}/trace_info.js +0 -0
  104. /package/src/{internal_utils → helpers}/trace_info.spec.js +0 -0
  105. /package/src/{internal_utils → helpers}/workflow_context.js +0 -0
  106. /package/src/{internal_utils → helpers}/workflow_context.spec.js +0 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * > [!WARNING]
3
+ * > **Internal use only.** Not part of the public API; may change without notice.
4
+ *
5
+ * These are helpers for other SDK modules integration.
6
+ * These need Temporal activity runtime to work, as they access state, emit events and use node:* modules.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ export * from './context.js';
11
+ export * from './events.js';
12
+ export * from './tracing.js';
@@ -0,0 +1,3 @@
1
+ export * from './context.js';
2
+ export * from './events.js';
3
+ export * from './tracing.js';
@@ -0,0 +1,46 @@
1
+ import { Attribute } from '#trace_attribute';
2
+
3
+ /**
4
+ * Tools to interact with Tracing
5
+ */
6
+ export declare const Tracing: {
7
+ Attribute: typeof Attribute;
8
+
9
+ /**
10
+ * Creates a new event.
11
+ *
12
+ * @param args
13
+ * @param args.id - A unique id for the Event.
14
+ * @param args.kind - The kind of Event, like HTTP, DiskWrite, DBOp, etc.
15
+ * @param args.name - The human-friendly name of the Event: query, request, create.
16
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "input" field.
17
+ */
18
+ addEventStart( args: { id: string; kind: string; name: string; details: unknown } ): void;
19
+
20
+ /**
21
+ * Concludes an event.
22
+ *
23
+ * @param args
24
+ * @param args.id - The id of the event to conclude.
25
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "output" field.
26
+ */
27
+ addEventEnd( args: { id: string; details: unknown } ): void;
28
+
29
+ /**
30
+ * Concludes an event with an error.
31
+ *
32
+ * @param args
33
+ * @param args.id - The id of the event to conclude.
34
+ * @param args.details - Arbitrary data to add to this event, it will be used as the "error" field.
35
+ */
36
+ addEventError( args: { id: string; details: unknown } ): void;
37
+
38
+ /**
39
+ * Adds an attribute to an event.
40
+ *
41
+ * @param args
42
+ * @param args.eventId - The id of the event to attach the attribute to.
43
+ * @param args.attribute - The attribute to attach to the event.
44
+ */
45
+ addEventAttribute( args: { eventId: string; attribute: Attribute.Instance } ): void;
46
+ };
@@ -0,0 +1,11 @@
1
+ import { addEventActionWithContext as send, EventAction } from '#tracing';
2
+
3
+ import { Attribute } from '#trace_attribute';
4
+
5
+ export const Tracing = {
6
+ Attribute,
7
+ addEventStart: ( { id, kind, name, details } ) => send( EventAction.START, { kind, name, details, id } ),
8
+ addEventEnd: ( { id, details } ) => send( EventAction.END, { id, details } ),
9
+ addEventError: ( { id, details } ) => send( EventAction.ERROR, { id, details } ),
10
+ addEventAttribute: ( { eventId, attribute } ) => send( EventAction.ADD_ATTR, { id: eventId, details: attribute } )
11
+ };
@@ -1,11 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
 
3
- vi.mock( '#utils', () => ( {
4
- throws: e => {
5
- throw e;
6
- }
7
- } ) );
8
-
9
3
  const logCalls = { warn: [], error: [] };
10
4
  vi.mock( '#logger', () => ( {
11
5
  createChildLogger: () => ( {
@@ -1,11 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
 
3
- vi.mock( '#utils', () => ( {
4
- throws: e => {
5
- throw e;
6
- }
7
- } ) );
8
-
9
3
  const getVarsMock = vi.fn();
10
4
  vi.mock( './configs', () => ( { getVars: () => getVarsMock() } ) );
11
5
 
@@ -1,7 +1,7 @@
1
1
  import { Storage } from '#async_storage';
2
2
  import { EventEmitter } from 'node:events';
3
3
  import { serializeError } from './tools/utils.js';
4
- import { isStringboolTrue } from '#utils';
4
+ import { isStringboolTrue } from '#helpers/string';
5
5
  import * as localProcessor from './processors/local/index.js';
6
6
  import * as s3Processor from './processors/s3/index.js';
7
7
  import { ComponentType } from '#consts';
@@ -3,7 +3,7 @@ import { WorkflowExecutionAlreadyStartedError, WorkflowIdConflictPolicy } from '
3
3
  import { WORKFLOW_CATALOG } from '#consts';
4
4
  import { catalogId, taskQueue } from '../configs.js';
5
5
  import { createChildLogger } from '#logger';
6
- import { CancellablePromise } from '#utils';
6
+ import { CancellablePromise } from '#helpers/promise';
7
7
 
8
8
  const log = createChildLogger( 'Catalog' );
9
9
 
@@ -7,9 +7,6 @@ vi.mock( '#consts', () => ( {
7
7
  METADATA_ACCESS_SYMBOL
8
8
  } ) );
9
9
 
10
- const setMetadata = ( target, values ) =>
11
- Object.defineProperty( target, METADATA_ACCESS_SYMBOL, { value: values, writable: false, enumerable: false, configurable: false } );
12
-
13
10
  describe( 'createCatalog', () => {
14
11
  it( 'builds catalog with activities grouped by workflow path and returns Catalog with CatalogWorkflow entries', async () => {
15
12
  const { createCatalog } = await import( './index.js' );
@@ -32,40 +29,40 @@ describe( 'createCatalog', () => {
32
29
  ];
33
30
 
34
31
  const activity1 = () => {};
35
- setMetadata( activity1, {
32
+ activity1[METADATA_ACCESS_SYMBOL] = {
36
33
  name: 'A1',
37
34
  path: '/flows/flow1#A1',
38
35
  description: 'desc-a1',
39
36
  inputSchema: z.object( { in: z.literal( 'a1' ) } ),
40
37
  outputSchema: z.object( { out: z.literal( 'a1' ) } )
41
- } );
38
+ };
42
39
 
43
40
  const activity2 = () => {};
44
- setMetadata( activity2, {
41
+ activity2[METADATA_ACCESS_SYMBOL] = {
45
42
  name: 'A2',
46
43
  path: '/flows/flow1#A2',
47
44
  description: 'desc-a2',
48
45
  inputSchema: z.object( { in: z.literal( 'a2' ) } ),
49
46
  outputSchema: z.object( { out: z.literal( 'a2' ) } )
50
- } );
47
+ };
51
48
 
52
49
  const activity3 = () => {};
53
- setMetadata( activity3, {
50
+ activity3[METADATA_ACCESS_SYMBOL] = {
54
51
  name: 'B1',
55
52
  path: '/flows/flow2#B1',
56
53
  description: 'desc-b1',
57
54
  inputSchema: z.object( { in: z.literal( 'b1' ) } ),
58
55
  outputSchema: z.object( { out: z.literal( 'b1' ) } )
59
- } );
56
+ };
60
57
 
61
58
  const activity4 = () => {};
62
- setMetadata( activity4, {
59
+ activity4[METADATA_ACCESS_SYMBOL] = {
63
60
  name: 'X',
64
61
  path: '/other#X',
65
62
  description: 'desc-x',
66
63
  inputSchema: z.object( { in: z.literal( 'x' ) } ),
67
64
  outputSchema: z.object( { out: z.literal( 'x' ) } )
68
- } );
65
+ };
69
66
 
70
67
  const activities = {
71
68
  '/flows/flow1#A1': activity1,
@@ -1,5 +1,5 @@
1
1
  import * as z from 'zod';
2
- import { isStringboolTrue } from '#utils';
2
+ import { isStringboolTrue } from '#helpers/string';
3
3
 
4
4
  class InvalidEnvVarsErrors extends Error { }
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { createChildLogger } from '#logger';
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
- import { CancellablePromise } from '#utils';
3
+ import { CancellablePromise } from '#helpers/promise';
4
4
 
5
5
  const log = createChildLogger( 'Connection' );
6
6
 
@@ -0,0 +1,14 @@
1
+ import { messageBus } from '#bus';
2
+ import { ACTIVITY_LOGGER_SYMBOL, BusEventType } from '#consts';
3
+ import { activityInfo as activityInfoFn } from '@temporalio/activity';
4
+ import { assignImmutableProperty } from '#helpers/object';
5
+
6
+ /**
7
+ * Sets global functions on globalThis
8
+ */
9
+ export const bindGlobalFunctions = () => {
10
+ /** Defines the activity logger function, accessible in activity context via logger interface */
11
+ assignImmutableProperty( globalThis, ACTIVITY_LOGGER_SYMBOL, ( { level, message, metadata } ) =>
12
+ messageBus.emit( BusEventType.ACTIVITY_LOG, { level, message, metadata, activityInfo: activityInfoFn() } )
13
+ );
14
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ const {
4
+ ACTIVITY_LOGGER_SYMBOL,
5
+ BusEventType
6
+ } = vi.hoisted( () => ( {
7
+ ACTIVITY_LOGGER_SYMBOL: Symbol( 'activity_logger' ),
8
+ BusEventType: {
9
+ ACTIVITY_LOG: 'activity:log'
10
+ }
11
+ } ) );
12
+
13
+ const messageBusMock = vi.hoisted( () => ( {
14
+ emit: vi.fn()
15
+ } ) );
16
+ const activityInfoMock = vi.hoisted( () => vi.fn( () => ( {
17
+ activityId: 'act-1',
18
+ activityType: 'myStep'
19
+ } ) ) );
20
+
21
+ vi.mock( '#consts', () => ( { ACTIVITY_LOGGER_SYMBOL, BusEventType } ) );
22
+ vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
23
+ vi.mock( '@temporalio/activity', () => ( {
24
+ activityInfo: activityInfoMock
25
+ } ) );
26
+
27
+ describe( 'worker/global_functions', () => {
28
+ beforeEach( () => {
29
+ vi.clearAllMocks();
30
+ delete globalThis[ACTIVITY_LOGGER_SYMBOL];
31
+ } );
32
+
33
+ it( 'binds activity logger global function that emits activity log events with metadata', async () => {
34
+ const { bindGlobalFunctions } = await import( './global_functions.js' );
35
+ const metadata = { requestId: 'req-1' };
36
+
37
+ bindGlobalFunctions();
38
+ globalThis[ACTIVITY_LOGGER_SYMBOL]( {
39
+ level: 'debug',
40
+ message: 'activity detail',
41
+ metadata
42
+ } );
43
+
44
+ expect( activityInfoMock ).toHaveBeenCalledTimes( 1 );
45
+ expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.ACTIVITY_LOG, {
46
+ level: 'debug',
47
+ message: 'activity detail',
48
+ metadata,
49
+ activityInfo: {
50
+ activityId: 'act-1',
51
+ activityType: 'myStep'
52
+ }
53
+ } );
54
+ } );
55
+ } );
@@ -17,7 +17,8 @@ import { messageBus } from '#bus';
17
17
  import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
20
- import { runOnce } from '#utils';
20
+ import { bindGlobalFunctions } from './global_functions.js';
21
+ import { runOnce } from '#helpers/function';
21
22
 
22
23
  import './log_hooks.js';
23
24
 
@@ -76,6 +77,8 @@ const execute = async () => {
76
77
  log.info( 'Using gRPC proxy', { targetHost: grpcProxy } );
77
78
  }
78
79
 
80
+ bindGlobalFunctions();
81
+
79
82
  state.connection = await NativeConnection.connect( { address, tls: Boolean( apiKey ), apiKey, proxy } );
80
83
 
81
84
  log.info( 'Creating connection monitor...' );
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  const {
4
4
  catalogJobInstance,
5
+ bindGlobalFunctionsMock,
5
6
  configValues,
6
7
  connectionMonitorInstance,
7
8
  createCatalogMock,
@@ -98,6 +99,7 @@ const {
98
99
 
99
100
  return {
100
101
  catalogJobInstance,
102
+ bindGlobalFunctionsMock: vi.fn(),
101
103
  configValues,
102
104
  connectionMonitorInstance,
103
105
  createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
@@ -145,6 +147,7 @@ vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptors
145
147
  vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
146
148
  vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
147
149
  vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
150
+ vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
148
151
  vi.mock( './connection_monitor.js', () => ( {
149
152
  TemporalConnectionMonitor: vi.fn( function () {
150
153
  return connectionMonitorInstance;
@@ -222,6 +225,10 @@ describe( 'worker/index', () => {
222
225
  apiKey: undefined,
223
226
  proxy: undefined
224
227
  } );
228
+ expect( bindGlobalFunctionsMock ).toHaveBeenCalledTimes( 1 );
229
+ expect( bindGlobalFunctionsMock.mock.invocationCallOrder[0] ).toBeLessThan(
230
+ NativeConnection.connect.mock.invocationCallOrder[0]
231
+ );
225
232
  expect( TemporalConnectionMonitor ).toHaveBeenCalledWith( mockConnection );
226
233
  expect( CatalogJob ).toHaveBeenCalledWith( {
227
234
  connection: mockConnection,
@@ -5,8 +5,8 @@ import { headersToObject } from './headers.js';
5
5
  import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
7
  import { messageBus } from '#bus';
8
- import { aggregateAttributes } from '#internal_utils/aggregations';
9
- import { buildApplicationFailureWithDetails } from '#internal_utils/errors';
8
+ import { aggregateAttributes } from '#helpers/aggregations';
9
+ import { buildApplicationFailureWithDetails } from '#helpers/errors';
10
10
 
11
11
  /*
12
12
  This interceptor wraps every activity execution with cross-cutting concerns:
@@ -77,20 +77,17 @@ export class ActivityExecutionInterceptor {
77
77
 
78
78
  const output = await Storage.runWithContext( async _ => next( input ), storageContext );
79
79
 
80
- messageBus.emit( BusEventType.ACTIVITY_END, { activityInfo, workflowDetails, outputActivityKind } );
80
+ const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
81
+
82
+ messageBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
81
83
  Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
82
84
 
83
- return {
84
- [ACTIVITY_WRAPPER_VERSION_FIELD]: 1,
85
- output,
86
- aggregations: state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null
87
- };
85
+ return { [ACTIVITY_WRAPPER_VERSION_FIELD]: 1, output, aggregations };
88
86
 
89
87
  } catch ( error ) {
90
- messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error } );
91
- Tracing.addEventError( { id: activityId, details: error, traceInfo } );
92
-
93
88
  const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
89
+ messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, aggregations, workflowDetails, outputActivityKind, error } );
90
+ Tracing.addEventError( { id: activityId, details: error, traceInfo } );
94
91
 
95
92
  throw aggregations ? buildApplicationFailureWithDetails( error, { aggregations } ) : error;
96
93
  } finally {
@@ -100,6 +100,12 @@ const httpRequestAttribute = {
100
100
  requestId: 'req-1'
101
101
  };
102
102
 
103
+ const httpRequestAggregations = {
104
+ cost: { total: 0 },
105
+ tokens: { total: 0 },
106
+ httpRequests: { total: 1 }
107
+ };
108
+
103
109
  describe( 'ActivityExecutionInterceptor', () => {
104
110
  beforeEach( () => {
105
111
  vi.clearAllMocks();
@@ -136,7 +142,7 @@ describe( 'ActivityExecutionInterceptor', () => {
136
142
  );
137
143
  expect( messageBusEmitMock ).toHaveBeenCalledWith(
138
144
  BusEventType.ACTIVITY_END,
139
- { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
145
+ { activityInfo: activityInfoMock, aggregations: null, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
140
146
  );
141
147
  expect( addEventStartMock ).toHaveBeenCalledWith( {
142
148
  id: 'act-1',
@@ -189,14 +195,16 @@ describe( 'ActivityExecutionInterceptor', () => {
189
195
 
190
196
  await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( {
191
197
  output: { result: 'ok' },
192
- aggregations: {
193
- cost: { total: 0 },
194
- tokens: { total: 0 },
195
- httpRequests: { total: 1 }
196
- },
198
+ aggregations: httpRequestAggregations,
197
199
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
198
200
  } );
199
201
 
202
+ expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, {
203
+ activityInfo: activityInfoMock,
204
+ aggregations: httpRequestAggregations,
205
+ workflowDetails: workflowDetailsMock,
206
+ outputActivityKind: 'step'
207
+ } );
200
208
  } );
201
209
 
202
210
  it( 'stores collected aggregations in ApplicationFailure details after failed execution', async () => {
@@ -215,15 +223,17 @@ describe( 'ActivityExecutionInterceptor', () => {
215
223
  message: 'step failed',
216
224
  type: 'Error',
217
225
  details: [ {
218
- aggregations: {
219
- cost: { total: 0 },
220
- tokens: { total: 0 },
221
- httpRequests: { total: 1 }
222
- }
226
+ aggregations: httpRequestAggregations
223
227
  } ],
224
228
  cause: error
225
229
  } );
226
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.objectContaining( { error } ) );
230
+ expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
231
+ activityInfo: activityInfoMock,
232
+ aggregations: httpRequestAggregations,
233
+ workflowDetails: workflowDetailsMock,
234
+ outputActivityKind: 'step',
235
+ error
236
+ } );
227
237
  expect( addEventErrorMock ).toHaveBeenCalledOnce();
228
238
  expect( addEventEndMock ).not.toHaveBeenCalled();
229
239
  } );
@@ -243,13 +253,7 @@ describe( 'ActivityExecutionInterceptor', () => {
243
253
 
244
254
  expect( thrown.details ).toEqual( [
245
255
  { domain: { reason: 'bad-input' } },
246
- {
247
- aggregations: {
248
- cost: { total: 0 },
249
- tokens: { total: 0 },
250
- httpRequests: { total: 1 }
251
- }
252
- }
256
+ { aggregations: httpRequestAggregations }
253
257
  ] );
254
258
  } );
255
259
 
@@ -300,13 +304,7 @@ describe( 'ActivityExecutionInterceptor', () => {
300
304
  nonRetryable: true,
301
305
  details: [
302
306
  { domain: { reason: 'bad-input' } },
303
- {
304
- aggregations: {
305
- cost: { total: 0 },
306
- tokens: { total: 0 },
307
- httpRequests: { total: 1 }
308
- }
309
- }
307
+ { aggregations: httpRequestAggregations }
310
308
  ]
311
309
  } );
312
310
  } );
@@ -324,6 +322,7 @@ describe( 'ActivityExecutionInterceptor', () => {
324
322
  expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
325
323
  expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
326
324
  activityInfo: activityInfoMock,
325
+ aggregations: null,
327
326
  workflowDetails: workflowDetailsMock,
328
327
  outputActivityKind: 'step',
329
328
  error
@@ -1,10 +1,10 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
2
  import { workflowInfo, proxySinks, ContinueAsNew, isCancellation } from '@temporalio/workflow';
3
3
  import { memoToHeaders } from './headers.js';
4
- import { deepMerge } from '#utils';
5
- import { buildApplicationFailureWithDetails } from '#internal_utils/errors';
4
+ import { deepMerge } from '#helpers/object';
5
+ import { buildApplicationFailureWithDetails } from '#helpers/errors';
6
6
  import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
7
- import { createWorkflowDetails } from '#internal_utils/temporal_context';
7
+ import { createWorkflowDetails } from '#helpers/temporal_context';
8
8
 
9
9
  // this is a dynamic generated file with activity configs overwrites
10
10
  import stepOptions from '../temp/__activity_options.js';
@@ -54,7 +54,7 @@ const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true }
54
54
  vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMock( ...args ) } ) );
55
55
 
56
56
  const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
57
- vi.mock( '#utils', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
57
+ vi.mock( '#helpers/object', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
58
58
 
59
59
  const stepOptionsDefault = {};
60
60
  vi.mock( '../temp/__activity_options.js', () => ( { default: stepOptionsDefault } ) );
@@ -1,5 +1,5 @@
1
1
  import { sep } from 'node:path';
2
- import { rxEscape } from '#utils';
2
+ import { rxEscape } from '#helpers/string';
3
3
 
4
4
  /**
5
5
  * Creates a matcher function that based on "path", matches:
@@ -47,6 +47,13 @@ messageBus.on( BusEventType.ACTIVITY_ERROR, ( { activityInfo, outputActivityKind
47
47
  } )
48
48
  );
49
49
 
50
+ messageBus.on( BusEventType.ACTIVITY_LOG, ( { level, message, metadata, activityInfo } ) =>
51
+ activityLog[level]( message, {
52
+ ...metadata ?? {},
53
+ ...serializedActivityFields( activityInfo )
54
+ } )
55
+ );
56
+
50
57
  /*
51
58
  ╔═════════════════╗
52
59
  ║ Workflow events ║
@@ -82,3 +89,10 @@ messageBus.on( BusEventType.WORKFLOW_ERROR, ( { workflowDetails, error } ) =>
82
89
  error: error.message
83
90
  } )
84
91
  );
92
+
93
+ messageBus.on( BusEventType.WORKFLOW_LOG, ( { level, message, metadata, workflowDetails } ) =>
94
+ workflowLog[level]( message, {
95
+ ...metadata ?? {},
96
+ ...serializeWorkflowFields( workflowDetails )
97
+ } )
98
+ );
@@ -6,8 +6,17 @@ import {
6
6
  WORKFLOW_CATALOG
7
7
  } from '#consts';
8
8
 
9
- const activityLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
10
- const workflowLogMock = vi.hoisted( () => ( { info: vi.fn(), error: vi.fn() } ) );
9
+ const createLoggerMock = vi.hoisted( () => () => ( {
10
+ error: vi.fn(),
11
+ warn: vi.fn(),
12
+ info: vi.fn(),
13
+ http: vi.fn(),
14
+ verbose: vi.fn(),
15
+ debug: vi.fn(),
16
+ silly: vi.fn()
17
+ } ) );
18
+ const activityLogMock = vi.hoisted( () => createLoggerMock() );
19
+ const workflowLogMock = vi.hoisted( () => createLoggerMock() );
11
20
  const createChildLoggerMock = vi.hoisted( () =>
12
21
  vi.fn( name => ( name === 'Activity' ? activityLogMock : workflowLogMock ) )
13
22
  );
@@ -125,6 +134,44 @@ describe( 'log_hooks', () => {
125
134
 
126
135
  expect( activityLogMock.error ).not.toHaveBeenCalled();
127
136
  } );
137
+
138
+ it( 'ACTIVITY_LOG logs dynamic levels with metadata and serialized activity fields', () => {
139
+ onHandlers[BusEventType.ACTIVITY_LOG]( {
140
+ activityInfo,
141
+ level: 'debug',
142
+ message: 'activity detail',
143
+ metadata: {
144
+ custom: 'value',
145
+ workflowId: 'metadata-workflow-id'
146
+ }
147
+ } );
148
+
149
+ expect( activityLogMock.debug ).toHaveBeenCalledTimes( 1 );
150
+ expect( activityLogMock.debug ).toHaveBeenCalledWith( 'activity detail', {
151
+ custom: 'value',
152
+ activityId: 'act-1',
153
+ activityType: 'myWorkflow#myStep',
154
+ workflowId: 'wf-1',
155
+ workflowType: 'myWorkflow',
156
+ runId: 'run-1'
157
+ } );
158
+ } );
159
+
160
+ it( 'ACTIVITY_LOG accepts omitted metadata', () => {
161
+ onHandlers[BusEventType.ACTIVITY_LOG]( {
162
+ activityInfo,
163
+ level: 'info',
164
+ message: 'activity detail'
165
+ } );
166
+
167
+ expect( activityLogMock.info ).toHaveBeenCalledWith( 'activity detail', {
168
+ activityId: 'act-1',
169
+ activityType: 'myWorkflow#myStep',
170
+ workflowId: 'wf-1',
171
+ workflowType: 'myWorkflow',
172
+ runId: 'run-1'
173
+ } );
174
+ } );
128
175
  } );
129
176
 
130
177
  describe( 'workflow events', () => {
@@ -210,5 +257,39 @@ describe( 'log_hooks', () => {
210
257
 
211
258
  expect( workflowLogMock.error ).not.toHaveBeenCalled();
212
259
  } );
260
+
261
+ it( 'WORKFLOW_LOG logs dynamic levels with metadata and serialized workflow fields', () => {
262
+ onHandlers[BusEventType.WORKFLOW_LOG]( {
263
+ workflowDetails,
264
+ level: 'warn',
265
+ message: 'workflow detail',
266
+ metadata: {
267
+ custom: 'value',
268
+ runId: 'metadata-run-id'
269
+ }
270
+ } );
271
+
272
+ expect( workflowLogMock.warn ).toHaveBeenCalledTimes( 1 );
273
+ expect( workflowLogMock.warn ).toHaveBeenCalledWith( 'workflow detail', {
274
+ custom: 'value',
275
+ workflowId: 'wf-1',
276
+ workflowType: 'myWorkflow',
277
+ runId: 'run-1'
278
+ } );
279
+ } );
280
+
281
+ it( 'WORKFLOW_LOG accepts omitted metadata', () => {
282
+ onHandlers[BusEventType.WORKFLOW_LOG]( {
283
+ workflowDetails,
284
+ level: 'info',
285
+ message: 'workflow detail'
286
+ } );
287
+
288
+ expect( workflowLogMock.info ).toHaveBeenCalledWith( 'workflow detail', {
289
+ workflowId: 'wf-1',
290
+ workflowType: 'myWorkflow',
291
+ runId: 'run-1'
292
+ } );
293
+ } );
213
294
  } );
214
295
  } );
@@ -1,7 +1,7 @@
1
1
  import { BusEventType, ComponentType } from '#consts';
2
2
  import * as Tracing from '#tracing';
3
3
  import { messageBus } from '#bus';
4
- import { createWorkflowDetails } from '#internal_utils/temporal_context';
4
+ import { createWorkflowDetails } from '#helpers/temporal_context';
5
5
 
6
6
  // This sink allow for sandbox Temporal environment to send trace logs back to the main thread.
7
7
  export const sinks = {
@@ -10,6 +10,12 @@ export const sinks = {
10
10
  * Workflow lifecycle sinks
11
11
  */
12
12
  workflow: {
13
+ log: {
14
+ fn: ( workflowInfo, { level, message, metadata } ) => {
15
+ messageBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
16
+ },
17
+ callDuringReplay: false
18
+ },
13
19
  start: {
14
20
  fn: ( workflowInfo, input ) => {
15
21
  const { runId, workflowType, memo: { traceInfo }, parent } = workflowInfo;