@outputai/core 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.1070949.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 (77) hide show
  1. package/package.json +8 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +5 -2
  5. package/src/errors.d.ts +12 -2
  6. package/src/errors.js +17 -3
  7. package/src/errors.spec.js +19 -0
  8. package/src/helpers/activity_options.js +21 -0
  9. package/src/helpers/activity_options.spec.js +55 -0
  10. package/src/helpers/error_serializer.js +236 -0
  11. package/src/helpers/error_serializer.spec.js +413 -0
  12. package/src/helpers/errors.js +23 -14
  13. package/src/helpers/errors.spec.js +31 -75
  14. package/src/helpers/fetch.js +1 -38
  15. package/src/helpers/fetch.spec.js +1 -39
  16. package/src/helpers/function.js +13 -0
  17. package/src/helpers/function.spec.js +23 -1
  18. package/src/helpers/object.js +5 -1
  19. package/src/helpers/object.spec.js +5 -0
  20. package/src/helpers/redact.js +51 -0
  21. package/src/helpers/redact.spec.js +66 -0
  22. package/src/helpers/string.js +10 -0
  23. package/src/helpers/string.spec.js +36 -1
  24. package/src/hooks/index.d.ts +76 -78
  25. package/src/hooks/index.js +57 -49
  26. package/src/hooks/index.spec.js +104 -105
  27. package/src/hooks/pending_hooks.js +24 -0
  28. package/src/hooks/pending_hooks.spec.js +62 -0
  29. package/src/interface/evaluator.d.ts +10 -9
  30. package/src/interface/evaluator.js +1 -6
  31. package/src/interface/evaluator.spec.js +10 -10
  32. package/src/interface/index.js +2 -1
  33. package/src/interface/step.d.ts +16 -15
  34. package/src/interface/step.js +1 -6
  35. package/src/interface/step.spec.js +10 -10
  36. package/src/interface/validations/index.js +33 -53
  37. package/src/interface/validations/index.spec.js +25 -17
  38. package/src/interface/webhook.js +1 -2
  39. package/src/interface/webhook.spec.js +25 -19
  40. package/src/interface/workflow.d.ts +25 -18
  41. package/src/interface/workflow.js +24 -52
  42. package/src/interface/workflow.spec.js +109 -108
  43. package/src/interface/workflow_activity_options.js +2 -5
  44. package/src/interface/workflow_utils.d.ts +15 -0
  45. package/src/interface/workflow_utils.js +19 -0
  46. package/src/interface/workflow_utils.spec.js +82 -1
  47. package/src/sdk/runtime/events.d.ts +4 -6
  48. package/src/sdk/runtime/events.js +2 -15
  49. package/src/sdk/runtime/events.spec.js +14 -92
  50. package/src/tracing/trace_engine.js +9 -11
  51. package/src/tracing/trace_engine.spec.js +67 -28
  52. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  53. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  54. package/src/worker/configs.js +0 -3
  55. package/src/worker/global_functions.js +2 -2
  56. package/src/worker/global_functions.spec.js +3 -3
  57. package/src/worker/index.js +20 -11
  58. package/src/worker/index.spec.js +29 -16
  59. package/src/worker/interceptors/activity.js +34 -21
  60. package/src/worker/interceptors/activity.spec.js +77 -172
  61. package/src/worker/interceptors/workflow.js +53 -11
  62. package/src/worker/interceptors/workflow.spec.js +176 -49
  63. package/src/worker/loader/workflows.js +16 -3
  64. package/src/worker/loader/workflows.spec.js +37 -5
  65. package/src/worker/log_hooks.js +22 -19
  66. package/src/worker/log_hooks.spec.js +14 -6
  67. package/src/worker/sinks.js +8 -8
  68. package/src/worker/sinks.spec.js +9 -9
  69. package/src/worker/telemetry.js +2 -1
  70. package/src/worker/temporal_logger.js +33 -0
  71. package/src/worker/temporal_logger.spec.js +90 -0
  72. package/src/helpers/aggregations.js +0 -44
  73. package/src/helpers/aggregations.spec.js +0 -92
  74. package/src/tracing/tools/utils.js +0 -42
  75. package/src/tracing/tools/utils.spec.js +0 -83
  76. package/src/worker/proxy.js +0 -20
  77. package/src/worker/proxy.spec.js +0 -51
@@ -1,6 +1,6 @@
1
1
  import { BusEventType, ComponentType } from '#consts';
2
2
  import * as Tracing from '#tracing';
3
- import { messageBus } from '#bus';
3
+ import { mainEventBus } from '#bus';
4
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.
@@ -12,15 +12,15 @@ export const sinks = {
12
12
  workflow: {
13
13
  log: {
14
14
  fn: ( workflowInfo, { level, message, metadata } ) => {
15
- messageBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
15
+ mainEventBus.emit( BusEventType.WORKFLOW_LOG, { level, message, metadata, workflowDetails: createWorkflowDetails( workflowInfo ) } );
16
16
  },
17
17
  callDuringReplay: false
18
18
  },
19
19
  start: {
20
20
  fn: ( workflowInfo, input ) => {
21
21
  const { runId, workflowType, memo: { traceInfo }, parent } = workflowInfo;
22
- messageBus.emit( BusEventType.WORKFLOW_START, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
23
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
22
+ mainEventBus.emit( BusEventType.WORKFLOW_START, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
23
+ if ( traceInfo ) {
24
24
  Tracing.addEventStart( {
25
25
  id: runId,
26
26
  kind: ComponentType.WORKFLOW,
@@ -37,8 +37,8 @@ export const sinks = {
37
37
  end: {
38
38
  fn: ( workflowInfo, output ) => {
39
39
  const { runId, memo: { traceInfo } } = workflowInfo;
40
- messageBus.emit( BusEventType.WORKFLOW_END, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
41
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
40
+ mainEventBus.emit( BusEventType.WORKFLOW_END, { workflowDetails: createWorkflowDetails( workflowInfo ) } );
41
+ if ( traceInfo ) {
42
42
  Tracing.addEventEnd( { id: runId, details: output, traceInfo } );
43
43
  }
44
44
  },
@@ -48,8 +48,8 @@ export const sinks = {
48
48
  error: {
49
49
  fn: ( workflowInfo, error ) => {
50
50
  const { runId, memo: { traceInfo } } = workflowInfo;
51
- messageBus.emit( BusEventType.WORKFLOW_ERROR, { workflowDetails: createWorkflowDetails( workflowInfo ), error } );
52
- if ( traceInfo ) { // internal workflows (catalog) do not have this info
51
+ mainEventBus.emit( BusEventType.WORKFLOW_ERROR, { workflowDetails: createWorkflowDetails( workflowInfo ), error } );
52
+ if ( traceInfo ) {
53
53
  Tracing.addEventError( { id: runId, details: error, traceInfo } );
54
54
  }
55
55
  },
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { BusEventType, ComponentType } from '#consts';
3
3
 
4
- const messageBusMock = vi.hoisted( () => ( {
4
+ const mainEventBusMock = vi.hoisted( () => ( {
5
5
  emit: vi.fn()
6
6
  } ) );
7
7
 
@@ -15,7 +15,7 @@ const createWorkflowDetailsMock = vi.hoisted( () => vi.fn( workflowInfo => ( {
15
15
  runId: workflowInfo.runId
16
16
  } ) ) );
17
17
 
18
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
18
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
19
19
  vi.mock( '#tracing', () => ( {
20
20
  addEventStart: addEventStartMock,
21
21
  addEventEnd: addEventEndMock,
@@ -69,7 +69,7 @@ describe( 'worker/sinks', () => {
69
69
  } );
70
70
 
71
71
  expect( createWorkflowDetailsMock ).toHaveBeenCalledWith( workflowInfo );
72
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_LOG, {
72
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_LOG, {
73
73
  level: 'info',
74
74
  message: 'workflow detail',
75
75
  metadata,
@@ -83,7 +83,7 @@ describe( 'worker/sinks', () => {
83
83
 
84
84
  sinks.workflow.start.fn( workflowInfo, input );
85
85
 
86
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
86
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
87
87
  expect( addEventStartMock ).toHaveBeenCalledWith( {
88
88
  id: 'run-1',
89
89
  kind: ComponentType.WORKFLOW,
@@ -99,7 +99,7 @@ describe( 'worker/sinks', () => {
99
99
 
100
100
  sinks.workflow.start.fn( { ...workflowInfo, memo: {} }, { value: 'input' } );
101
101
 
102
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
102
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_START, { workflowDetails } );
103
103
  expect( addEventStartMock ).not.toHaveBeenCalled();
104
104
  } );
105
105
 
@@ -109,7 +109,7 @@ describe( 'worker/sinks', () => {
109
109
 
110
110
  sinks.workflow.end.fn( workflowInfo, output );
111
111
 
112
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
112
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
113
113
  expect( addEventEndMock ).toHaveBeenCalledWith( {
114
114
  id: 'run-1',
115
115
  details: output,
@@ -122,7 +122,7 @@ describe( 'worker/sinks', () => {
122
122
 
123
123
  sinks.workflow.end.fn( { ...workflowInfo, memo: {} }, { value: 'output' } );
124
124
 
125
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
125
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_END, { workflowDetails } );
126
126
  expect( addEventEndMock ).not.toHaveBeenCalled();
127
127
  } );
128
128
 
@@ -132,7 +132,7 @@ describe( 'worker/sinks', () => {
132
132
 
133
133
  sinks.workflow.error.fn( workflowInfo, error );
134
134
 
135
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
135
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
136
136
  expect( addEventErrorMock ).toHaveBeenCalledWith( {
137
137
  id: 'run-1',
138
138
  details: error,
@@ -146,7 +146,7 @@ describe( 'worker/sinks', () => {
146
146
 
147
147
  sinks.workflow.error.fn( { ...workflowInfo, memo: {} }, error );
148
148
 
149
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
149
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, { workflowDetails, error } );
150
150
  expect( addEventErrorMock ).not.toHaveBeenCalled();
151
151
  } );
152
152
 
@@ -1,3 +1,4 @@
1
+ import { serializeError } from '#helpers/error_serializer';
1
2
  import { createChildLogger } from '#logger';
2
3
  import { workerTelemetryIntervalMs } from './configs.js';
3
4
 
@@ -18,7 +19,7 @@ export const setupTelemetry = ( { worker } ) => {
18
19
  }
19
20
  } );
20
21
  } catch ( error ) {
21
- log.warn( 'Failure', { error: error.message } );
22
+ log.warn( 'Failure', { error: serializeError( error, { dropKeys: [ 'stack' ] } ) } );
22
23
  }
23
24
  }, workerTelemetryIntervalMs ).unref();
24
25
  };
@@ -0,0 +1,33 @@
1
+ import { DefaultLogger, makeTelemetryFilterString, Runtime } from '@temporalio/worker';
2
+ import { createChildLogger } from '#logger';
3
+
4
+ const log = createChildLogger( 'Temporal' );
5
+ const OMITTED_MESSAGES = [ 'Activity failed', 'Workflow failed' ];
6
+
7
+ const temporalLevelToInternalLevelMap = {
8
+ TRACE: 'debug',
9
+ DEBUG: 'debug',
10
+ INFO: 'info',
11
+ WARN: 'warn',
12
+ ERROR: 'error'
13
+ };
14
+
15
+ export const setupTemporalLogger = () => {
16
+ const level = process.env.OUTPUT_TEMPORAL_LOG_LEVEL?.toUpperCase() ?? 'INFO';
17
+
18
+ Runtime.install( {
19
+ logger: new DefaultLogger( level, entry => {
20
+ // Already reported by Output activity logs and traces.
21
+ if ( OMITTED_MESSAGES.includes( entry.message ) ) {
22
+ return;
23
+ }
24
+ log.log( temporalLevelToInternalLevelMap[entry.level], entry.message, entry.meta );
25
+ } ),
26
+ telemetryOptions: {
27
+ logging: {
28
+ forward: {},
29
+ filter: makeTelemetryFilterString( { core: level, other: level } )
30
+ }
31
+ }
32
+ } );
33
+ };
@@ -0,0 +1,90 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ defaultLoggerMock,
5
+ logMock,
6
+ makeTelemetryFilterStringMock,
7
+ runtimeInstallMock
8
+ } = vi.hoisted( () => ( {
9
+ defaultLoggerMock: vi.fn( function ( level, callback ) {
10
+ this.level = level;
11
+ this.callback = callback;
12
+ } ),
13
+ logMock: { log: vi.fn() },
14
+ makeTelemetryFilterStringMock: vi.fn().mockReturnValue( 'temporal-filter' ),
15
+ runtimeInstallMock: vi.fn()
16
+ } ) );
17
+
18
+ vi.mock( '@temporalio/worker', () => ( {
19
+ DefaultLogger: defaultLoggerMock,
20
+ makeTelemetryFilterString: makeTelemetryFilterStringMock,
21
+ Runtime: { install: runtimeInstallMock }
22
+ } ) );
23
+ vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
24
+
25
+ import { setupTemporalLogger } from './temporal_logger.js';
26
+
27
+ describe( 'setupTemporalLogger', () => {
28
+ beforeEach( () => {
29
+ vi.clearAllMocks();
30
+ vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', undefined );
31
+ } );
32
+
33
+ afterEach( () => {
34
+ vi.unstubAllEnvs();
35
+ } );
36
+
37
+ it( 'forwards Temporal SDK and Core logs through the application logger', () => {
38
+ setupTemporalLogger();
39
+
40
+ expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'INFO', other: 'INFO' } );
41
+ expect( defaultLoggerMock ).toHaveBeenCalledWith( 'INFO', expect.any( Function ) );
42
+ expect( runtimeInstallMock ).toHaveBeenCalledWith( {
43
+ logger: expect.any( defaultLoggerMock ),
44
+ telemetryOptions: {
45
+ logging: {
46
+ forward: {},
47
+ filter: 'temporal-filter'
48
+ }
49
+ }
50
+ } );
51
+
52
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
53
+ entryCallback( { level: 'WARN', message: 'Worker warning', meta: { taskQueue: 'default' } } );
54
+
55
+ expect( logMock.log ).toHaveBeenCalledWith( 'warn', 'Worker warning', { taskQueue: 'default' } );
56
+ } );
57
+
58
+ it.each( [
59
+ [ 'TRACE', 'debug' ],
60
+ [ 'DEBUG', 'debug' ],
61
+ [ 'INFO', 'info' ],
62
+ [ 'WARN', 'warn' ],
63
+ [ 'ERROR', 'error' ]
64
+ ] )( 'maps Temporal %s logs to the %s application level', ( temporalLevel, internalLevel ) => {
65
+ setupTemporalLogger();
66
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
67
+
68
+ entryCallback( { level: temporalLevel, message: `${temporalLevel} event`, meta: { source: 'test' } } );
69
+
70
+ expect( logMock.log ).toHaveBeenCalledWith( internalLevel, `${temporalLevel} event`, { source: 'test' } );
71
+ } );
72
+
73
+ it( 'applies a case-insensitive configured log level to SDK and Core logs', () => {
74
+ vi.stubEnv( 'OUTPUT_TEMPORAL_LOG_LEVEL', 'debug' );
75
+
76
+ setupTemporalLogger();
77
+
78
+ expect( defaultLoggerMock ).toHaveBeenCalledWith( 'DEBUG', expect.any( Function ) );
79
+ expect( makeTelemetryFilterStringMock ).toHaveBeenCalledWith( { core: 'DEBUG', other: 'DEBUG' } );
80
+ } );
81
+
82
+ it.each( [ 'Activity failed', 'Workflow failed' ] )( 'omits redundant "%s" events', message => {
83
+ setupTemporalLogger();
84
+ const [ entryCallback ] = defaultLoggerMock.mock.calls[0].slice( 1 );
85
+
86
+ entryCallback( { level: 'WARN', message, meta: { error: new Error( 'failed' ) } } );
87
+
88
+ expect( logMock.log ).not.toHaveBeenCalled();
89
+ } );
90
+ } );
@@ -1,44 +0,0 @@
1
- import { Attribute } from '#trace_attribute';
2
- import Decimal from 'decimal.js';
3
-
4
- /**
5
- * @typedef {object} Aggregation
6
- *
7
- * @property {object} cost
8
- * @property {object} cost.total - Total cost
9
- * @property {object} tokens
10
- * @property {object} tokens.total - Total tokens used
11
- * @property {object} tokens.input - Total input tokens used
12
- * @property {object} tokens.input_cached - Total cached input tokens used
13
- * @property {object} tokens.output - Total output tokens used
14
- * @property {object} tokens.reasoning - Total reasoning tokens used
15
- * @property {object} httpRequests
16
- * @property {object} httpRequests.total - Total number of http requests made
17
- */
18
- /**
19
- * Aggregates a collection of Attributes into a object with totals
20
- *
21
- * @param {Attribute} attributes
22
- * @returns {Aggregation} aggregation object
23
- */
24
- export const aggregateAttributes = attributes => ( {
25
- cost: {
26
- total: attributes
27
- .filter( a => [ Attribute.HTTPRequestCost.TYPE, Attribute.LLMUsage.TYPE ].includes( a.type ) )
28
- .reduce( ( sum, a ) => sum.add( a.total ), Decimal( 0 ) ).toNumber()
29
- },
30
- tokens: {
31
- total: attributes
32
- .filter( a => Attribute.LLMUsage.TYPE === a.type )
33
- .reduce( ( sum, a ) => sum.add( a.tokensUsed ), Decimal( 0 ) ).toNumber(),
34
- ...Object.entries( attributes
35
- .filter( a => Attribute.LLMUsage.TYPE === a.type )
36
- .flatMap( a => a.usage )
37
- .reduce( ( obj, a ) => Object.assign( obj, { [a.type]: ( obj[a.type] ?? Decimal( 0 ) ).add( a.amount ) } ), {} ) )
38
- .reduce( ( obj, [ k, v ] ) => Object.assign( obj, { [k]: v.toNumber() } ), {} ) // convert all values to number
39
-
40
- },
41
- httpRequests: {
42
- total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
43
- }
44
- } );
@@ -1,92 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Attribute } from '#trace_attribute';
3
- import { aggregateAttributes } from './aggregations.js';
4
-
5
- describe( 'aggregateAttributes', () => {
6
- it( 'returns zeroed aggregations when there are no attributes', () => {
7
- expect( aggregateAttributes( [] ) ).toEqual( {
8
- cost: { total: 0 },
9
- tokens: { total: 0 },
10
- httpRequests: { total: 0 }
11
- } );
12
- } );
13
-
14
- it( 'aggregates costs, token usage, and HTTP request count by attribute type', () => {
15
- const attributes = [
16
- {
17
- type: Attribute.HTTPRequestCount.TYPE,
18
- url: 'https://api.example.test/a',
19
- requestId: 'req-1'
20
- },
21
- {
22
- type: Attribute.HTTPRequestCount.TYPE,
23
- url: 'https://api.example.test/b',
24
- requestId: 'req-2'
25
- },
26
- {
27
- type: Attribute.HTTPRequestCost.TYPE,
28
- url: 'https://api.example.test/a',
29
- requestId: 'req-1',
30
- total: 0.2
31
- },
32
- {
33
- type: Attribute.LLMUsage.TYPE,
34
- modelId: 'gpt-4o',
35
- total: 0.3,
36
- tokensUsed: 120,
37
- usage: [
38
- { type: 'input', ppm: 1, amount: 100, total: 0.1 },
39
- { type: 'output', ppm: 2, amount: 20, total: 0.2 }
40
- ]
41
- },
42
- {
43
- type: Attribute.LLMUsage.TYPE,
44
- modelId: 'gpt-4o-mini',
45
- total: 0.05,
46
- tokensUsed: 30,
47
- usage: [
48
- { type: 'input', ppm: 1, amount: 25, total: 0.025 },
49
- { type: 'reasoning', ppm: 5, amount: 5, total: 0.025 }
50
- ]
51
- },
52
- {
53
- type: 'unrelated',
54
- total: 100,
55
- tokensUsed: 100
56
- }
57
- ];
58
-
59
- expect( aggregateAttributes( attributes ) ).toEqual( {
60
- cost: { total: 0.55 },
61
- tokens: {
62
- total: 150,
63
- input: 125,
64
- output: 20,
65
- reasoning: 5
66
- },
67
- httpRequests: { total: 2 }
68
- } );
69
- } );
70
-
71
- it( 'uses LLMUsage.tokensUsed for total tokens instead of summing usage amounts', () => {
72
- const attributes = [
73
- {
74
- type: Attribute.LLMUsage.TYPE,
75
- modelId: 'provider-model',
76
- total: 0.1,
77
- tokensUsed: 42,
78
- usage: [
79
- { type: 'input', ppm: 1, amount: 10, total: 0.01 },
80
- { type: 'output', ppm: 1, amount: 5, total: 0.005 }
81
- ]
82
- }
83
- ];
84
-
85
- expect( aggregateAttributes( attributes ).tokens ).toEqual( {
86
- total: 42,
87
- input: 10,
88
- output: 5
89
- } );
90
- } );
91
- } );
92
-
@@ -1,42 +0,0 @@
1
- import { inspect } from 'node:util';
2
-
3
- /**
4
- * @typedef {object} SerializedError
5
- * @property {string} name - The error constructor name
6
- * @property {string} message - The error message
7
- * @property {string} stack - The error stack trace
8
- */
9
-
10
- /**
11
- * Recursively Serialize an error object. Navigate using "cause" property.
12
- *
13
- * Goes up 10 levels deep.
14
- *
15
- * @param {Error} error
16
- * @returns {SerializedError}
17
- */
18
- export const serializeError = ( () => {
19
-
20
- const serializeValue = v => {
21
- try {
22
- return JSON.parse( JSON.stringify( v ) );
23
- } catch {
24
- return inspect( v, { depth: 5, breakLength: Infinity, colors: false } );
25
- }
26
- };
27
-
28
- return ( error, depth = 0 ) => {
29
- if ( depth > 10 ) {
30
- return { name: 'Error', message: 'Cause chain too deep' };
31
- }
32
- if ( error instanceof Error ) {
33
- return {
34
- name: error.constructor.name,
35
- message: error.message,
36
- stack: error.stack,
37
- ...( error.cause !== undefined ? { cause: serializeError( error.cause, depth + 1 ) } : {} )
38
- };
39
- }
40
- return serializeValue( error );
41
- };
42
- } )();
@@ -1,83 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { serializeError } from './utils.js';
3
-
4
- describe( 'serializeError', () => {
5
- it( 'serializes basic Error fields', () => {
6
- const error = new Error( 'boom' );
7
-
8
- const result = serializeError( error );
9
-
10
- expect( result ).toMatchObject( {
11
- name: 'Error',
12
- message: 'boom'
13
- } );
14
- expect( typeof result.stack ).toBe( 'string' );
15
- expect( result ).not.toHaveProperty( 'cause' );
16
- } );
17
-
18
- it( 'preserves custom error constructor names', () => {
19
- class CustomError extends Error {}
20
- const error = new CustomError( 'custom boom' );
21
-
22
- expect( serializeError( error ) ).toMatchObject( {
23
- name: 'CustomError',
24
- message: 'custom boom'
25
- } );
26
- } );
27
-
28
- it( 'recursively serializes Error causes', () => {
29
- const root = new TypeError( 'root failure' );
30
- const wrapped = new Error( 'wrapped failure', { cause: root } );
31
-
32
- expect( serializeError( wrapped ) ).toMatchObject( {
33
- name: 'Error',
34
- message: 'wrapped failure',
35
- cause: {
36
- name: 'TypeError',
37
- message: 'root failure'
38
- }
39
- } );
40
- } );
41
-
42
- it( 'preserves JSON-serializable non-Error causes', () => {
43
- const cause = {
44
- code: 'bad_input',
45
- path: [ 'items', 0, 'title' ],
46
- message: 'Expected title'
47
- };
48
- const error = new Error( 'validation failed', { cause } );
49
-
50
- expect( serializeError( error ) ).toMatchObject( {
51
- name: 'Error',
52
- message: 'validation failed',
53
- cause
54
- } );
55
- } );
56
-
57
- it( 'falls back to inspect for non-JSON-serializable causes', () => {
58
- const cause = { name: 'circular' };
59
- cause.self = cause;
60
- const error = new Error( 'failed', { cause } );
61
-
62
- const result = serializeError( error );
63
-
64
- expect( result.cause ).toContain( 'circular' );
65
- expect( result.cause ).toContain( 'Circular' );
66
- } );
67
-
68
- it( 'falls back to inspect for primitive values JSON cannot serialize', () => {
69
- expect( serializeError( 1n ) ).toBe( '1n' );
70
- } );
71
-
72
- it( 'stops serializing cause chains after the depth limit', () => {
73
- const makeErrorChain = depth => depth === 0 ?
74
- new Error( 'leaf' ) :
75
- new Error( `level ${depth}`, { cause: makeErrorChain( depth - 1 ) } );
76
- const getCauseAtDepth = ( error, depth ) => depth === 0 ?
77
- error :
78
- getCauseAtDepth( error.cause, depth - 1 );
79
-
80
- expect( getCauseAtDepth( serializeError( makeErrorChain( 11 ) ), 11 ) )
81
- .toEqual( { name: 'Error', message: 'Cause chain too deep' } );
82
- } );
83
- } );
@@ -1,20 +0,0 @@
1
- import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
2
- import { createChildLogger } from '#logger';
3
-
4
- const log = createChildLogger( 'Proxy' );
5
-
6
- /**
7
- * Routes all `fetch()` calls (including those inside Temporal activities)
8
- * through an HTTP/HTTPS proxy when standard proxy env vars are set.
9
- * No-op when none are set.
10
- */
11
- export const bootstrapFetchProxy = () => {
12
- const httpProxyUrl = process.env.http_proxy ?? process.env.HTTP_PROXY;
13
- const httpsProxyUrl = process.env.https_proxy ?? process.env.HTTPS_PROXY;
14
-
15
- if ( httpProxyUrl?.length > 0 || httpsProxyUrl?.length > 0 ) {
16
- log.info( 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent', { httpProxyUrl, httpsProxyUrl } );
17
- /* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
18
- setGlobalDispatcher( new EnvHttpProxyAgent( { allowH2: false } ) );
19
- }
20
- };
@@ -1,51 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const mockSetGlobalDispatcher = vi.fn();
4
- const MockEnvHttpProxyAgent = vi.fn();
5
- const mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
6
-
7
- vi.mock( 'undici', () => ( {
8
- EnvHttpProxyAgent: MockEnvHttpProxyAgent,
9
- setGlobalDispatcher: mockSetGlobalDispatcher
10
- } ) );
11
-
12
- vi.mock( '#logger', () => ( {
13
- createChildLogger: () => mockLogger
14
- } ) );
15
-
16
- describe( 'worker/proxy', () => {
17
- const proxyEnv = [ 'http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY' ];
18
-
19
- beforeEach( () => {
20
- vi.clearAllMocks();
21
- for ( const key of proxyEnv ) {
22
- delete process.env[key];
23
- }
24
- } );
25
-
26
- afterEach( () => {
27
- vi.resetModules();
28
- } );
29
-
30
- it( 'does nothing when no proxy env vars are set', async () => {
31
- const { bootstrapFetchProxy } = await import( './proxy.js' );
32
- bootstrapFetchProxy();
33
-
34
- expect( MockEnvHttpProxyAgent ).not.toHaveBeenCalled();
35
- expect( mockSetGlobalDispatcher ).not.toHaveBeenCalled();
36
- expect( mockLogger.info ).not.toHaveBeenCalled();
37
- } );
38
-
39
- it( 'sets global dispatcher when proxy URL is available', async () => {
40
- process.env.HTTP_PROXY = 'http://proxy:8080/';
41
- const { bootstrapFetchProxy } = await import( './proxy.js' );
42
- bootstrapFetchProxy();
43
-
44
- expect( MockEnvHttpProxyAgent ).toHaveBeenCalledWith( { allowH2: false } );
45
- expect( mockSetGlobalDispatcher ).toHaveBeenCalledTimes( 1 );
46
- expect( mockLogger.info ).toHaveBeenCalledWith(
47
- 'Proxy env vars detected, setting up global fetch dispatcher EnvHttpProxyAgent',
48
- { httpProxyUrl: 'http://proxy:8080/', httpsProxyUrl: undefined }
49
- );
50
- } );
51
- } );