@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 (63) hide show
  1. package/package.json +12 -7
  2. package/src/consts.js +5 -2
  3. package/src/errors.d.ts +12 -2
  4. package/src/errors.js +17 -3
  5. package/src/errors.spec.js +19 -0
  6. package/src/helpers/activity_options.js +21 -0
  7. package/src/helpers/activity_options.spec.js +55 -0
  8. package/src/helpers/error_serializer.js +236 -0
  9. package/src/helpers/error_serializer.spec.js +413 -0
  10. package/src/helpers/errors.js +55 -13
  11. package/src/helpers/errors.spec.js +166 -66
  12. package/src/helpers/fetch.js +1 -38
  13. package/src/helpers/fetch.spec.js +1 -39
  14. package/src/helpers/function.js +13 -0
  15. package/src/helpers/function.spec.js +23 -1
  16. package/src/helpers/redact.js +51 -0
  17. package/src/helpers/redact.spec.js +66 -0
  18. package/src/helpers/string.js +10 -0
  19. package/src/helpers/string.spec.js +36 -1
  20. package/src/hooks/index.d.ts +35 -44
  21. package/src/hooks/index.js +2 -1
  22. package/src/hooks/index.spec.js +15 -7
  23. package/src/interface/evaluator.d.ts +10 -9
  24. package/src/interface/evaluator.js +1 -6
  25. package/src/interface/evaluator.spec.js +10 -10
  26. package/src/interface/step.d.ts +16 -15
  27. package/src/interface/step.js +1 -6
  28. package/src/interface/step.spec.js +10 -10
  29. package/src/interface/validations/index.js +33 -53
  30. package/src/interface/validations/index.spec.js +25 -17
  31. package/src/interface/webhook.js +1 -2
  32. package/src/interface/webhook.spec.js +25 -19
  33. package/src/interface/workflow.d.ts +25 -18
  34. package/src/interface/workflow.js +24 -52
  35. package/src/interface/workflow.spec.js +109 -108
  36. package/src/interface/workflow_activity_options.js +2 -5
  37. package/src/temporal/client.js +47 -0
  38. package/src/temporal/client.spec.js +124 -0
  39. package/src/temporal/index.d.ts +32 -0
  40. package/src/temporal/index.js +1 -0
  41. package/src/tracing/trace_engine.js +9 -11
  42. package/src/tracing/trace_engine.spec.js +67 -28
  43. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  44. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  45. package/src/worker/index.js +10 -1
  46. package/src/worker/index.spec.js +23 -8
  47. package/src/worker/interceptors/activity.js +32 -19
  48. package/src/worker/interceptors/activity.spec.js +71 -166
  49. package/src/worker/interceptors/workflow.js +53 -11
  50. package/src/worker/interceptors/workflow.spec.js +176 -49
  51. package/src/worker/loader/workflows.js +16 -3
  52. package/src/worker/loader/workflows.spec.js +37 -5
  53. package/src/worker/log_hooks.js +13 -10
  54. package/src/worker/log_hooks.spec.js +12 -4
  55. package/src/worker/sinks.js +3 -1
  56. package/src/worker/sinks.spec.js +21 -4
  57. package/src/worker/telemetry.js +2 -1
  58. package/src/worker/temporal_logger.js +33 -0
  59. package/src/worker/temporal_logger.spec.js +90 -0
  60. package/src/helpers/aggregations.js +0 -44
  61. package/src/helpers/aggregations.spec.js +0 -92
  62. package/src/tracing/tools/utils.js +0 -42
  63. package/src/tracing/tools/utils.spec.js +0 -83
@@ -1,9 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { ApplicationFailure } from '@temporalio/common';
3
- import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType } from '#consts';
4
- import { Attribute } from '#trace_attribute';
2
+ import { ApplicationFailure, CompleteAsyncError } from '@temporalio/common';
3
+ import { ActivitySpecialOutput, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
4
+ import { TransparentFatalError } from '#errors';
5
5
 
6
- const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
7
6
  const heartbeatMock = vi.hoisted( () => vi.fn() );
8
7
  const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
9
8
  const activityInfoMock = vi.hoisted( () => ( {
@@ -64,15 +63,6 @@ vi.mock( './headers.js', () => ( {
64
63
  const mainEventBusEmitMock = vi.fn();
65
64
  vi.mock( '#bus', () => ( { mainEventBus: { emit: mainEventBusEmitMock } } ) );
66
65
 
67
- vi.mock( '#consts', async importOriginal => {
68
- const actual = await importOriginal();
69
- return {
70
- ...actual, get METADATA_ACCESS_SYMBOL() {
71
- return METADATA_ACCESS_SYMBOL;
72
- }
73
- };
74
- } );
75
-
76
66
  vi.mock( '../configs.js', () => ( {
77
67
  get activityHeartbeatEnabled() {
78
68
  return process.env.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED !== 'false';
@@ -93,24 +83,12 @@ const makeInput = () => ( {
93
83
  headers: {}
94
84
  } );
95
85
 
96
- const httpRequestAttribute = {
97
- type: Attribute.HTTPRequestCount.TYPE,
98
- url: 'https://api.example.test/items',
99
- requestId: 'req-1'
100
- };
101
-
102
- const httpRequestAggregations = {
103
- cost: { total: 0 },
104
- tokens: { total: 0 },
105
- httpRequests: { total: 1 }
106
- };
107
-
108
86
  describe( 'ActivityExecutionInterceptor', () => {
109
87
  beforeEach( () => {
110
88
  vi.clearAllMocks();
111
89
  activityInfoMock.workflowType = 'myWorkflow';
90
+ delete activityInfoMock.retryPolicy;
112
91
  vi.useFakeTimers();
113
- vi.resetModules();
114
92
  // Default: heartbeat enabled with 50ms interval for fast tests
115
93
  vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'true' );
116
94
  vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS', '50' );
@@ -126,22 +104,14 @@ describe( 'ActivityExecutionInterceptor', () => {
126
104
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
127
105
  const next = vi.fn().mockResolvedValue( { result: 'ok' } );
128
106
 
129
- const promise = interceptor.execute( makeInput(), next );
130
- vi.advanceTimersByTime( 0 );
131
- const output = await promise;
132
-
133
- expect( output ).toEqual( {
134
- output: { result: 'ok' },
135
- aggregations: null,
136
- [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
137
- } );
107
+ await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( { result: 'ok' } );
138
108
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
139
109
  BusEventType.ACTIVITY_START,
140
110
  { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
141
111
  );
142
112
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
143
113
  BusEventType.ACTIVITY_END,
144
- { activityInfo: activityInfoMock, aggregations: null, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
114
+ { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
145
115
  );
146
116
  expect( addEventStartMock ).toHaveBeenCalledWith( {
147
117
  id: 'act-1',
@@ -161,74 +131,36 @@ describe( 'ActivityExecutionInterceptor', () => {
161
131
  workflowDetails: workflowDetailsMock,
162
132
  outputActivityKind: 'step',
163
133
  workflowFilename: '/workflows/myWorkflow.js',
164
- traceInfo: traceInfoMock,
165
- addAttribute: expect.any( Function )
134
+ traceInfo: traceInfoMock
166
135
  } )
167
136
  );
168
137
  } );
169
138
 
170
- it( 'handles next returning a non-Promise value', async () => {
171
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
172
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
173
- const next = vi.fn( () => ( { result: 'sync' } ) );
174
-
175
- await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( {
176
- output: { result: 'sync' },
177
- aggregations: null,
178
- [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
179
- } );
180
-
181
- expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
182
- expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, traceInfo: traceInfoMock } );
183
- expect( addEventErrorMock ).not.toHaveBeenCalled();
184
- } );
185
-
186
- it( 'returns collected aggregations after successful execution', async () => {
187
- runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
188
- ctx.addAttribute( httpRequestAttribute );
189
- return fn();
190
- } );
191
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
192
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
193
- const next = vi.fn().mockResolvedValue( { result: 'ok' } );
194
-
195
- await expect( interceptor.execute( makeInput(), next ) ).resolves.toEqual( {
196
- output: { result: 'ok' },
197
- aggregations: httpRequestAggregations,
198
- [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
199
- } );
200
-
201
- expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, {
202
- activityInfo: activityInfoMock,
203
- aggregations: httpRequestAggregations,
204
- workflowDetails: workflowDetailsMock,
205
- outputActivityKind: 'step'
206
- } );
207
- } );
208
-
209
- it( 'stores collected aggregations in ApplicationFailure details after failed execution', async () => {
210
- runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
211
- ctx.addAttribute( httpRequestAttribute );
212
- return fn();
213
- } );
139
+ it( 'wraps native errors with serialized details', async () => {
214
140
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
215
141
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
216
142
  const error = new Error( 'step failed' );
143
+ error.code = 'ESTEP';
217
144
  const next = vi.fn().mockRejectedValue( error );
218
145
 
219
146
  const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
147
+
220
148
  expect( thrown ).toBeInstanceOf( ApplicationFailure );
221
149
  expect( thrown ).toMatchObject( {
222
150
  message: 'step failed',
223
151
  type: 'Error',
224
152
  details: [ {
225
- aggregations: httpRequestAggregations
153
+ error: {
154
+ name: 'Error',
155
+ message: 'step failed',
156
+ code: 'ESTEP'
157
+ }
226
158
  } ],
227
- cause: error
159
+ cause: error,
160
+ nonRetryable: false
228
161
  } );
229
162
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
230
163
  activityInfo: activityInfoMock,
231
- aggregations: httpRequestAggregations,
232
164
  workflowDetails: workflowDetailsMock,
233
165
  outputActivityKind: 'step',
234
166
  error
@@ -237,105 +169,93 @@ describe( 'ActivityExecutionInterceptor', () => {
237
169
  expect( addEventEndMock ).not.toHaveBeenCalled();
238
170
  } );
239
171
 
240
- it( 'appends collected aggregations to existing failure details', async () => {
241
- runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
242
- ctx.addAttribute( httpRequestAttribute );
243
- return fn();
244
- } );
172
+ it( 'marks inherited configured error types as non-retryable', async () => {
173
+ class ValidationError extends Error {}
174
+ class SpecificValidationError extends ValidationError {}
175
+ activityInfoMock.retryPolicy = { nonRetryableErrorTypes: [ 'ValidationError' ] };
245
176
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
246
177
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
247
- const error = new Error( 'step failed' );
248
- error.details = [ { domain: { reason: 'bad-input' } } ];
178
+ const error = new SpecificValidationError( 'invalid input' );
249
179
  const next = vi.fn().mockRejectedValue( error );
250
180
 
251
181
  const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
252
182
 
253
- expect( thrown.details ).toEqual( [
254
- { domain: { reason: 'bad-input' } },
255
- { aggregations: httpRequestAggregations }
256
- ] );
257
- } );
258
-
259
- it( 'rethrows the original error when failed execution collected no attributes', async () => {
260
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
261
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
262
- const error = new Error( 'step failed' );
263
- const next = vi.fn().mockRejectedValue( error );
264
-
265
- await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
266
- } );
267
-
268
- it( 'rethrows the original error with existing details when failed execution collected no attributes', async () => {
269
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
270
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
271
- const error = new Error( 'step failed' );
272
- error.details = [ { domain: { reason: 'bad-input' } } ];
273
- const next = vi.fn().mockRejectedValue( error );
274
-
275
- await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
276
- expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
277
- } );
278
-
279
- it( 'wraps existing ApplicationFailure without creating a self-cause', async () => {
280
- runWithContextMock.mockImplementationOnce( async ( fn, ctx ) => {
281
- ctx.addAttribute( httpRequestAttribute );
282
- return fn();
283
- } );
284
- const error = ApplicationFailure.create( {
285
- message: 'application failed',
286
- type: 'OriginalType',
183
+ expect( thrown ).toMatchObject( {
184
+ type: 'SpecificValidationError',
287
185
  nonRetryable: true,
288
- details: [ { domain: { reason: 'bad-input' } } ]
186
+ cause: error
289
187
  } );
188
+ } );
189
+
190
+ it( 'uses a transparent error cause while remaining non-retryable without retry policy metadata', async () => {
290
191
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
291
192
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
193
+ const cause = new TypeError( 'provider rejected request' );
194
+ cause.code = 'EAUTH';
195
+ const error = new TransparentFatalError( cause );
292
196
  const next = vi.fn().mockRejectedValue( error );
293
197
 
294
198
  const thrown = await interceptor.execute( makeInput(), next ).catch( e => e );
295
199
 
296
- expect( thrown ).toBeInstanceOf( ApplicationFailure );
297
- expect( thrown ).not.toBe( error );
298
- expect( thrown.cause ).toBe( error );
299
- expect( thrown.cause ).not.toBe( thrown );
300
200
  expect( thrown ).toMatchObject( {
301
- message: 'application failed',
302
- type: 'OriginalType',
201
+ message: 'provider rejected request',
202
+ type: 'TypeError',
303
203
  nonRetryable: true,
304
- details: [
305
- { domain: { reason: 'bad-input' } },
306
- { aggregations: httpRequestAggregations }
307
- ]
204
+ cause,
205
+ details: [ {
206
+ error: {
207
+ name: 'TypeError',
208
+ message: 'provider rejected request',
209
+ code: 'EAUTH'
210
+ }
211
+ } ]
212
+ } );
213
+ expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: cause, traceInfo: traceInfoMock } );
214
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
215
+ activityInfo: activityInfoMock,
216
+ workflowDetails: workflowDetailsMock,
217
+ outputActivityKind: 'step',
218
+ error: cause
308
219
  } );
309
220
  } );
310
221
 
311
- it( 'records trace error event on failed execution', async () => {
222
+ it( 'rethrows existing Temporal failures unchanged', async () => {
312
223
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
313
224
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
314
- const error = new Error( 'step failed' );
225
+ const error = ApplicationFailure.nonRetryable( 'domain failed', 'DomainFailure', { reason: 'invalid' } );
315
226
  const next = vi.fn().mockRejectedValue( error );
316
227
 
317
- const promise = interceptor.execute( makeInput(), next );
318
- vi.advanceTimersByTime( 0 );
319
-
320
- await expect( promise ).rejects.toThrow( 'step failed' );
321
- expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
228
+ await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
229
+ expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
322
230
  expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
323
231
  activityInfo: activityInfoMock,
324
- aggregations: null,
325
232
  workflowDetails: workflowDetailsMock,
326
233
  outputActivityKind: 'step',
327
234
  error
328
235
  } );
329
- expect( addEventStartMock ).toHaveBeenCalledOnce();
330
- expect( addEventErrorMock ).toHaveBeenCalledWith( { id: 'act-1', details: error, traceInfo: traceInfoMock } );
331
- expect( addEventEndMock ).not.toHaveBeenCalled();
332
236
  } );
333
237
 
334
- it( 'sends periodic heartbeats during activity execution', async () => {
238
+ it( 'records async completion handoff as a trace end only', async () => {
239
+ const { ActivityExecutionInterceptor } = await import( './activity.js' );
240
+ const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
241
+ const error = new CompleteAsyncError();
242
+ const next = vi.fn().mockRejectedValue( error );
243
+
244
+ await expect( interceptor.execute( makeInput(), next ) ).rejects.toBe( error );
245
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
246
+ expect( mainEventBusEmitMock ).toHaveBeenCalledTimes( 1 );
247
+ expect( addEventEndMock ).toHaveBeenCalledWith( {
248
+ id: 'act-1',
249
+ details: ActivitySpecialOutput.ASYNC_HANDOFF,
250
+ traceInfo: traceInfoMock
251
+ } );
252
+ expect( addEventErrorMock ).not.toHaveBeenCalled();
253
+ } );
254
+
255
+ it( 'sends periodic heartbeats and stops after successful execution', async () => {
335
256
  const { ActivityExecutionInterceptor } = await import( './activity.js' );
336
257
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
337
258
 
338
- // next() resolves only after we manually resolve it, simulating a long-running activity
339
259
  const deferred = { resolve: null };
340
260
  const next = vi.fn().mockImplementation( () => new Promise( r => {
341
261
  deferred.resolve = r;
@@ -356,16 +276,6 @@ describe( 'ActivityExecutionInterceptor', () => {
356
276
 
357
277
  deferred.resolve( { result: 'done' } );
358
278
  await promise;
359
- } );
360
-
361
- it( 'clears heartbeat interval after activity completes', async () => {
362
- const { ActivityExecutionInterceptor } = await import( './activity.js' );
363
- const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
364
- const next = vi.fn().mockResolvedValue( { result: 'ok' } );
365
-
366
- const promise = interceptor.execute( makeInput(), next );
367
- vi.advanceTimersByTime( 0 );
368
- await promise;
369
279
 
370
280
  heartbeatMock.mockClear();
371
281
  vi.advanceTimersByTime( 500 );
@@ -379,7 +289,7 @@ describe( 'ActivityExecutionInterceptor', () => {
379
289
 
380
290
  const promise = interceptor.execute( makeInput(), next );
381
291
  vi.advanceTimersByTime( 0 );
382
- await promise.catch( () => {} );
292
+ await expect( promise ).rejects.toBeInstanceOf( ApplicationFailure );
383
293
 
384
294
  heartbeatMock.mockClear();
385
295
  vi.advanceTimersByTime( 500 );
@@ -391,7 +301,6 @@ describe( 'ActivityExecutionInterceptor', () => {
391
301
  const workflows = [ { name: 'myWorkflow', path: '/workflows/myWorkflow.js', aliases: [ 'myWorkflowOld' ] } ];
392
302
  const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows } );
393
303
 
394
- // Override context to use alias as workflowType
395
304
  activityInfoMock.workflowType = 'myWorkflowOld';
396
305
  const next = vi.fn().mockResolvedValue( { result: 'ok' } );
397
306
 
@@ -399,14 +308,10 @@ describe( 'ActivityExecutionInterceptor', () => {
399
308
  vi.advanceTimersByTime( 0 );
400
309
  await promise;
401
310
 
402
- // Should resolve to the correct path despite using the alias
403
311
  expect( runWithContextMock ).toHaveBeenCalledWith(
404
312
  expect.any( Function ),
405
313
  expect.objectContaining( { workflowFilename: '/workflows/myWorkflow.js' } )
406
314
  );
407
-
408
- // Restore for other tests
409
- activityInfoMock.workflowType = 'myWorkflow';
410
315
  } );
411
316
 
412
317
  it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
@@ -1,13 +1,17 @@
1
1
  // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
- import { workflowInfo, proxySinks, ContinueAsNew, isCancellation } from '@temporalio/workflow';
2
+ import { workflowInfo, proxySinks, ContinueAsNew, isCancellation, ApplicationFailure, TemporalFailure, upsertMemo } from '@temporalio/workflow';
3
3
  import { memoToHeaders } from './headers.js';
4
4
  import { deepMerge } from '#helpers/object';
5
- import { buildApplicationFailureWithDetails } from '#helpers/errors';
6
- import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
5
+ import { WorkflowSpecialOutput } from '#consts';
7
6
  import { createWorkflowDetails } from '#helpers/temporal_context';
7
+ import { TraceInfo } from '#helpers/trace_info';
8
+ import { FatalError, TransparentFatalError } from '#errors';
9
+ import { serializeError } from '#helpers/error_serializer';
10
+ import { enforceActivityOptions } from '#helpers/activity_options';
8
11
 
9
- // this is a dynamic generated file with activity configs overwrites
12
+ // these are a dynamic generated file with activity configs overwrites and workflow options
10
13
  import activityOptionsMap from '../temp/__activity_options.js';
14
+ import workflowOptionsMap from '../temp/__workflow_options.js';
11
15
 
12
16
  /*
13
17
  This interceptor adds Memo and serialized workflowInfo() to the Activity invocation headers.
@@ -27,6 +31,8 @@ class HeadersInjectionInterceptor {
27
31
  if ( activityOptionsOverrides ) {
28
32
  input.options = deepMerge( input.options ?? {}, activityOptionsOverrides );
29
33
  }
34
+ // Re-enforce framework options after component overrides
35
+ input.options = enforceActivityOptions( input.options );
30
36
  return next( input );
31
37
  }
32
38
  };
@@ -35,6 +41,15 @@ const sinks = proxySinks();
35
41
 
36
42
  class WorkflowExecutionInterceptor {
37
43
  async execute( input, next ) {
44
+ const { workflowType, root } = workflowInfo();
45
+ const traceEnabled = workflowOptionsMap[workflowType]?.disableTrace !== true;
46
+ const isRoot = !root; // root workflow doesn't have the root block
47
+
48
+ upsertMemo( {
49
+ payloadVersion: '2',
50
+ ...( isRoot && traceEnabled && { traceInfo: TraceInfo.build() } )
51
+ } );
52
+
38
53
  sinks.workflow.start( input.args[0] );
39
54
  try {
40
55
  const output = await next( input );
@@ -51,19 +66,46 @@ class WorkflowExecutionInterceptor {
51
66
  throw error;
52
67
  }
53
68
 
69
+ /**
70
+ * We send serialized error to the sink, because temporal loses Error meta-information, like name, type, etc
71
+ * when crossing the workflow sandbox barrier. The plain object doesn't lose anything.
72
+ */
73
+ /**
74
+ * This happens when the workflow is cancelled.
75
+ * Re-throw the error as it is.
76
+ * Sink back the error without stack (not necessary for this).
77
+ */
54
78
  if ( isCancellation( error ) ) {
55
- sinks.workflow.error( error );
79
+ sinks.workflow.error( serializeError( error, { dropKeys: [ 'stack' ] } ) );
56
80
  throw error;
57
81
  }
58
82
 
59
- sinks.workflow.error( error );
83
+ /**
84
+ * This represents an error in the workflow itself, in this case the sink receives the error serialize with stack
85
+ * An Application failure is constructed to finish the workflow run and the serialized error is added to its details
86
+ * so the API or other consumers can read it. Stack is omitted
87
+ */
88
+ if ( error instanceof FatalError ) {
89
+ const unwrappedError = error instanceof TransparentFatalError ? error.cause : error;
90
+ sinks.workflow.error( serializeError( unwrappedError ) );
91
+ throw ApplicationFailure.fromError( unwrappedError, {
92
+ cause: unwrappedError,
93
+ nonRetryable: true,
94
+ details: [ { error: serializeError( unwrappedError, { dropKeys: [ 'stack' ] } ) } ]
95
+ } );
96
+ };
60
97
 
61
- /*
62
- * Add internal error .details to Temporal's ApplicationFailure .details
63
- * This make it possible for this information be retrieved by Temporal's client instance.
64
- * Ref: https://typescript.temporal.io/api/classes/common.ApplicationFailure#details
98
+ /**
99
+ * This is mostly likely an error in one Activity (or other Temporal parts)
100
+ * Re-throw as it is and sink back the error without failure, for the same reason as in cancellation.
65
101
  */
66
- throw error[METADATA_ACCESS_SYMBOL] ? buildApplicationFailureWithDetails( error, error[METADATA_ACCESS_SYMBOL] ) : error;
102
+ if ( error instanceof TemporalFailure ) {
103
+ sinks.workflow.error( serializeError( error ) );
104
+ throw error;
105
+ }
106
+
107
+ // Workflow Task failure, do not sink as this retries the Task only, not the Workflow
108
+ throw error;
67
109
  }
68
110
  }
69
111
  };