@shipfox/api-server 7.0.1 → 7.1.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
@@ -2,7 +2,7 @@
2
2
  "name": "@shipfox/api-server",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "7.0.1",
5
+ "version": "7.1.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -25,46 +25,37 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@shipfox/annotations": "6.0.0",
28
+ "@shipfox/annotations": "7.1.0",
29
+ "@shipfox/api-agent": "7.1.0",
29
30
  "@shipfox/annotations-dto": "6.0.0",
30
- "@shipfox/api-agent": "6.0.0",
31
31
  "@shipfox/api-agent-dto": "6.0.0",
32
- "@shipfox/api-auth": "7.0.1",
33
- "@shipfox/api-auth-dto": "6.0.0",
34
- "@shipfox/api-definitions": "6.0.0",
32
+ "@shipfox/api-auth": "7.1.0",
33
+ "@shipfox/api-auth-dto": "7.1.0",
35
34
  "@shipfox/api-definitions-dto": "6.0.0",
36
- "@shipfox/api-dispatcher": "6.0.0",
37
- "@shipfox/api-email-challenges": "0.2.2",
38
- "@shipfox/api-integration-core": "7.0.1",
39
- "@shipfox/api-integration-core-dto": "6.0.0",
40
- "@shipfox/api-logs": "6.0.0",
41
- "@shipfox/api-projects": "6.0.0",
35
+ "@shipfox/api-dispatcher": "7.1.0",
36
+ "@shipfox/api-email-challenges": "0.3.0",
37
+ "@shipfox/api-definitions": "7.1.0",
38
+ "@shipfox/api-integration-core": "7.1.0",
39
+ "@shipfox/api-projects": "7.1.0",
40
+ "@shipfox/api-logs": "7.1.0",
42
41
  "@shipfox/api-projects-dto": "6.0.0",
43
- "@shipfox/api-runners": "7.0.1",
42
+ "@shipfox/api-runners": "7.1.0",
44
43
  "@shipfox/api-runners-dto": "7.0.1",
45
- "@shipfox/api-secrets": "6.0.0",
44
+ "@shipfox/api-secrets": "7.1.0",
45
+ "@shipfox/api-integration-core-dto": "6.0.0",
46
+ "@shipfox/api-workflows": "7.1.0",
46
47
  "@shipfox/api-secrets-dto": "6.0.0",
47
- "@shipfox/api-triggers": "7.0.1",
48
- "@shipfox/api-workflows": "7.0.1",
49
- "@shipfox/api-workflows-dto": "6.0.0",
48
+ "@shipfox/api-triggers": "7.1.0",
50
49
  "@shipfox/api-workspaces-dto": "6.0.0",
51
- "@shipfox/api-workspaces": "7.0.1",
50
+ "@shipfox/api-workflows-dto": "6.0.0",
51
+ "@shipfox/api-workspaces": "7.1.0",
52
52
  "@shipfox/config": "1.2.2",
53
- "@shipfox/node-error-monitoring": "0.1.3",
54
- "@shipfox/node-fastify": "0.2.4",
53
+ "@shipfox/node-fastify": "0.3.0",
54
+ "@shipfox/node-error-monitoring": "0.2.0",
55
55
  "@shipfox/node-jwt": "0.3.0",
56
- "@shipfox/node-module": "0.4.0",
57
- "@shipfox/node-opentelemetry": "0.5.2",
58
- "@shipfox/node-postgres": "0.4.2"
59
- },
60
- "devDependencies": {
61
- "@shipfox/biome": "1.8.2",
62
- "@shipfox/depcruise": "1.0.2",
63
- "@shipfox/inter-module": "0.2.0",
64
- "@shipfox/swc": "1.2.6",
65
- "@shipfox/ts-config": "1.3.8",
66
- "@shipfox/typescript": "1.1.7",
67
- "@shipfox/vitest": "1.2.3"
56
+ "@shipfox/node-postgres": "0.4.2",
57
+ "@shipfox/node-module": "0.5.0",
58
+ "@shipfox/node-opentelemetry": "0.6.0"
68
59
  },
69
60
  "scripts": {
70
61
  "build": "shipfox-swc",
package/src/index.ts CHANGED
@@ -1,3 +1,8 @@
1
- export {type DefaultModulesOptions, defaultModules} from './modules.js';
1
+ export {
2
+ type DefaultModulesOptions,
3
+ type DefaultRunnersModuleFactory,
4
+ defaultModules,
5
+ } from './modules.js';
6
+ export {createLoginMethodsRoute} from './routes/login-methods.js';
2
7
  export type {CreateServerOptions, RunServerOptions, ServerHandle} from './server.js';
3
8
  export {createServer, runServer} from './server.js';
@@ -1,8 +1,12 @@
1
+ import {createRequire} from 'node:module';
1
2
  import {startInstanceInstrumentation} from '@shipfox/node-opentelemetry';
2
3
 
4
+ const {version} = createRequire(import.meta.url)('../package.json') as {version: string};
5
+
3
6
  // The metrics API has no proxy meter: instruments created before this preload
4
7
  // completes bind to a no-op provider for the process lifetime.
5
8
  await startInstanceInstrumentation({
6
9
  serviceName: 'api',
7
- instrumentations: {fastify: true, http: true, pg: true},
10
+ serviceVersion: version,
11
+ instrumentations: {fastify: true, http: true, pg: true, awsSdk: true},
8
12
  });
@@ -254,6 +254,40 @@ describe('defaultModules', () => {
254
254
  expect(mocks.createRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
255
255
  });
256
256
 
257
+ it('lets a host replace only the Runners module with the composed Auth client', async () => {
258
+ const policy = {filterEligibleWorkspaceIds: vi.fn().mockResolvedValue(new Set<string>())};
259
+ const runnersModule = mocks.createRunnersModule({});
260
+ const createHostRunnersModule = vi.fn(({auth}) =>
261
+ mocks.createRunnersModule({auth, installationProvisioning: {policy}}),
262
+ );
263
+ mocks.createRunnersModule.mockClear();
264
+
265
+ const modules = await defaultModules({runnersModule: createHostRunnersModule});
266
+
267
+ expect(createHostRunnersModule).toHaveBeenCalledWith({auth: expect.any(Object)});
268
+ expect(mocks.createRunnersModule).toHaveBeenCalledWith({
269
+ auth: createHostRunnersModule.mock.calls[0]?.[0].auth,
270
+ installationProvisioning: {policy},
271
+ });
272
+ expect(modules.filter((module) => module.name === 'runners')).toEqual([runnersModule]);
273
+ expect(modules.map((module) => module.name)).toEqual([
274
+ 'email-challenges',
275
+ 'auth',
276
+ 'workspaces',
277
+ 'secrets',
278
+ 'agent',
279
+ 'integrations',
280
+ 'projects',
281
+ 'definitions',
282
+ 'workflows',
283
+ 'annotations',
284
+ 'runners',
285
+ 'logs',
286
+ 'triggers',
287
+ 'dispatcher',
288
+ ]);
289
+ });
290
+
257
291
  it('injects Workflows into integrations and logs and namespaces provider secrets', async () => {
258
292
  await defaultModules();
259
293
 
package/src/modules.ts CHANGED
@@ -4,7 +4,10 @@ import {createAgentModule} from '@shipfox/api-agent';
4
4
  import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
5
5
  import {createAuthModule} from '@shipfox/api-auth';
6
6
  import {config as authConfig} from '@shipfox/api-auth/config';
7
- import {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
7
+ import {
8
+ type AuthInterModuleClient,
9
+ authInterModuleContract,
10
+ } from '@shipfox/api-auth-dto/inter-module';
8
11
  import {createDefinitionsModule} from '@shipfox/api-definitions';
9
12
  import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
10
13
  import {dispatcherModule} from '@shipfox/api-dispatcher';
@@ -23,21 +26,38 @@ import {createWorkflowsModule} from '@shipfox/api-workflows';
23
26
  import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
24
27
  import {workspacesModule} from '@shipfox/api-workspaces';
25
28
  import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
29
+ import {reportError} from '@shipfox/node-error-monitoring';
26
30
  import {durationToSeconds} from '@shipfox/node-jwt';
27
31
  import type {ShipfoxModule} from '@shipfox/node-module';
28
32
  import {
29
33
  createInMemoryInterModuleTransport,
30
34
  registerInterModulePresentations,
31
35
  } from '@shipfox/node-module/inter-module';
36
+ import {logger} from '@shipfox/node-opentelemetry';
32
37
 
33
38
  export interface DefaultModulesOptions {
34
39
  webhookDeliverySource?: WebhookDeliverySource | undefined;
40
+ runnersModule?: DefaultRunnersModuleFactory | undefined;
35
41
  }
36
42
 
43
+ export type DefaultRunnersModuleFactory = (options: {auth: AuthInterModuleClient}) => ShipfoxModule;
44
+
37
45
  export async function defaultModules(
38
46
  options: DefaultModulesOptions = {},
39
47
  ): Promise<ShipfoxModule[]> {
40
- const interModuleTransport = createInMemoryInterModuleTransport();
48
+ const interModuleTransport = createInMemoryInterModuleTransport({
49
+ reportInternalError: (error, context) => {
50
+ logger().error(
51
+ {err: error, module: context.module, method: context.method, phase: context.phase},
52
+ 'Inter-module call failed unexpectedly',
53
+ );
54
+ reportError(error, {
55
+ boundary: 'inter-module',
56
+ operation: `${context.module}.${context.method}`,
57
+ tags: {module: context.module, method: context.method, phase: context.phase},
58
+ });
59
+ },
60
+ });
41
61
  const workflowsClient = interModuleTransport.createClient(workflowsInterModuleContract);
42
62
  const authClient = interModuleTransport.createClient(authInterModuleContract);
43
63
  const agentClient = interModuleTransport.createClient(agentInterModuleContract);
@@ -154,7 +174,7 @@ export async function defaultModules(
154
174
  integrations: integrationsClient,
155
175
  }),
156
176
  annotationsModule,
157
- createRunnersModule({auth: authClient}),
177
+ (options.runnersModule ?? createRunnersModule)({auth: authClient}),
158
178
  createLogsModule({
159
179
  workflows: workflowsClient,
160
180
  jobLeaseTokenTtlSeconds: durationToSeconds(authConfig.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN),
@@ -0,0 +1,56 @@
1
+ import {createApp} from '@shipfox/node-fastify';
2
+ import type {LoginMethod} from '@shipfox/node-module';
3
+ import {createLoginMethodsRoute} from './login-methods.js';
4
+
5
+ describe('createLoginMethodsRoute', () => {
6
+ it('lists registered methods exactly once, including unknown future IDs', async () => {
7
+ const app = await createApp({
8
+ auth: [],
9
+ routes: [
10
+ createLoginMethodsRoute({
11
+ loginMethods: [{id: 'password'}, {id: 'oauth-google'}, {id: 'future-provider'}],
12
+ }),
13
+ ],
14
+ swagger: false,
15
+ });
16
+
17
+ try {
18
+ const response = await app.inject({method: 'GET', url: '/auth/login-methods'});
19
+
20
+ expect(response.statusCode).toBe(200);
21
+ expect(response.json()).toEqual({
22
+ login_methods: [{id: 'password'}, {id: 'oauth-google'}, {id: 'future-provider'}],
23
+ });
24
+ } finally {
25
+ await app.close();
26
+ }
27
+ });
28
+
29
+ it('does not serialize module implementation details', async () => {
30
+ const loginMethod = {
31
+ id: 'oauth-google',
32
+ clientSecret: 'not-for-clients',
33
+ callbackUrl: 'https://api.example.test/auth/callback',
34
+ } as LoginMethod;
35
+ const app = await createApp({
36
+ auth: [],
37
+ routes: [createLoginMethodsRoute({loginMethods: [loginMethod]})],
38
+ swagger: false,
39
+ });
40
+
41
+ try {
42
+ const response = await app.inject({method: 'GET', url: '/auth/login-methods'});
43
+
44
+ expect(response.statusCode).toBe(200);
45
+ expect(response.json()).toEqual({login_methods: [{id: 'oauth-google'}]});
46
+ expect(response.body).not.toContain('not-for-clients');
47
+ expect(response.body).not.toContain('api.example.test');
48
+ } finally {
49
+ await app.close();
50
+ }
51
+ });
52
+
53
+ it('rejects an out-of-contract login method ID before the server can start', () => {
54
+ expect(() => createLoginMethodsRoute({loginMethods: [{id: ''}]})).toThrow();
55
+ });
56
+ });
@@ -0,0 +1,21 @@
1
+ import {loginMethodsResponseSchema} from '@shipfox/api-auth-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import type {LoginMethod} from '@shipfox/node-module';
4
+
5
+ export function createLoginMethodsRoute({loginMethods}: {loginMethods: readonly LoginMethod[]}) {
6
+ const response = loginMethodsResponseSchema.parse({
7
+ login_methods: loginMethods.map(({id}) => ({id})),
8
+ });
9
+
10
+ return defineRoute({
11
+ method: 'GET',
12
+ path: '/auth/login-methods',
13
+ description: 'List the login methods available in this server composition.',
14
+ schema: {
15
+ response: {
16
+ 200: loginMethodsResponseSchema,
17
+ },
18
+ },
19
+ handler: () => response,
20
+ });
21
+ }
@@ -12,11 +12,13 @@ const mocks = vi.hoisted(() => {
12
12
  const workersHandle = {stop: vi.fn()};
13
13
  const servicesHandle = {stop: vi.fn()};
14
14
  return {
15
- captureException: vi.fn(),
15
+ markErrorReported: vi.fn(),
16
+ reportError: vi.fn(),
16
17
  closeApp: vi.fn(),
17
18
  closeErrorMonitoring: vi.fn(),
18
19
  closePostgresClient: vi.fn(),
19
20
  createApp: vi.fn(),
21
+ defineRoute: vi.fn((route) => route),
20
22
  createE2eAdminAuthMethod: vi.fn(),
21
23
  createE2eRouteGroup: vi.fn(),
22
24
  createPostgresClient: vi.fn(),
@@ -42,12 +44,14 @@ const mocks = vi.hoisted(() => {
42
44
  });
43
45
 
44
46
  vi.mock('@shipfox/node-error-monitoring', () => ({
45
- captureException: mocks.captureException,
46
47
  closeErrorMonitoring: mocks.closeErrorMonitoring,
48
+ markErrorReported: mocks.markErrorReported,
49
+ reportError: mocks.reportError,
47
50
  }));
48
51
  vi.mock('@shipfox/node-fastify', () => ({
49
52
  closeApp: mocks.closeApp,
50
53
  createApp: mocks.createApp,
54
+ defineRoute: mocks.defineRoute,
51
55
  listen: mocks.listen,
52
56
  }));
53
57
  vi.mock('@shipfox/node-module', () => ({
@@ -113,11 +117,13 @@ function lastStartModuleServicesOptions(): {
113
117
 
114
118
  function resetMocks(): void {
115
119
  vi.useRealTimers();
116
- mocks.captureException.mockReset();
120
+ mocks.markErrorReported.mockReset();
121
+ mocks.reportError.mockReset();
117
122
  mocks.closeApp.mockReset();
118
123
  mocks.closeErrorMonitoring.mockReset();
119
124
  mocks.closePostgresClient.mockReset();
120
125
  mocks.createApp.mockReset();
126
+ mocks.defineRoute.mockReset();
121
127
  mocks.createE2eAdminAuthMethod.mockReset();
122
128
  mocks.createE2eRouteGroup.mockReset();
123
129
  mocks.createPostgresClient.mockReset();
@@ -143,6 +149,7 @@ function resetMocks(): void {
143
149
  mocks.closeErrorMonitoring.mockResolvedValue(true);
144
150
  mocks.closePostgresClient.mockResolvedValue(undefined);
145
151
  mocks.createApp.mockResolvedValue({});
152
+ mocks.defineRoute.mockImplementation((route) => route);
146
153
  mocks.aggregateLoginMethods.mockReturnValue([{id: 'test-login'}]);
147
154
  mocks.createE2eRouteGroup.mockReturnValue([]);
148
155
  mocks.initializeModules.mockResolvedValue({
@@ -204,9 +211,16 @@ describe('createServer', () => {
204
211
  });
205
212
  expect(mocks.createApp).toHaveBeenCalledWith({
206
213
  auth: [],
207
- routes: [],
214
+ routes: [
215
+ expect.objectContaining({
216
+ method: 'GET',
217
+ path: '/auth/login-methods',
218
+ }),
219
+ ],
208
220
  fastifyOptions: {trustProxy: false},
209
221
  });
222
+ const appConfig = mocks.createApp.mock.calls.at(0)?.[0];
223
+ expect(appConfig?.routes[0].handler()).toEqual({login_methods: [{id: 'test-login'}]});
210
224
  });
211
225
 
212
226
  it('fails before startup side effects when no login method is configured', async () => {
@@ -492,7 +506,10 @@ describe('createServer', () => {
492
506
  {err: failure, taskQueue: 'runtime-queue'},
493
507
  'Module worker stopped unexpectedly',
494
508
  );
495
- expect(mocks.captureException).toHaveBeenCalledWith(failure);
509
+ expect(mocks.reportError).toHaveBeenCalledWith(failure, {
510
+ boundary: 'api.runtime',
511
+ tags: {taskQueue: 'runtime-queue'},
512
+ });
496
513
  expect(mocks.closeApp).toHaveBeenCalledOnce();
497
514
  expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
498
515
  expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
@@ -513,7 +530,10 @@ describe('createServer', () => {
513
530
  {err: failure, service: 'runtime-service'},
514
531
  'Module service stopped unexpectedly',
515
532
  );
516
- expect(mocks.captureException).toHaveBeenCalledWith(failure);
533
+ expect(mocks.reportError).toHaveBeenCalledWith(failure, {
534
+ boundary: 'api.runtime',
535
+ tags: {service: 'runtime-service'},
536
+ });
517
537
  expect(mocks.closeApp).toHaveBeenCalledOnce();
518
538
  expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
519
539
  expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {captureException, closeErrorMonitoring} from '@shipfox/node-error-monitoring';
1
+ import {closeErrorMonitoring, markErrorReported, reportError} from '@shipfox/node-error-monitoring';
2
2
  import {closeApp, createApp, listen} from '@shipfox/node-fastify';
3
3
  import {
4
4
  aggregateLoginMethods,
@@ -17,6 +17,7 @@ import {logger, shutdownServiceMetrics, startServiceMetrics} from '@shipfox/node
17
17
  import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
18
18
  import {config, parseApiTrustProxy} from './config.js';
19
19
  import {createE2eAdminAuthMethod, createE2eRouteGroup} from './e2e.js';
20
+ import {createLoginMethodsRoute} from './routes/login-methods.js';
20
21
 
21
22
  const RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS = 10_000;
22
23
  const ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS = 2_000;
@@ -46,7 +47,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
46
47
  hasActiveServer = true;
47
48
 
48
49
  try {
49
- aggregateLoginMethods({modules: options.modules});
50
+ const loginMethods = aggregateLoginMethods({modules: options.modules});
50
51
  startServiceMetrics({serviceName: 'api'});
51
52
  createPostgresClient();
52
53
 
@@ -62,7 +63,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
62
63
  logger().info('Creating HTTP server');
63
64
  await createApp({
64
65
  auth: [...auth, ...e2eAuth],
65
- routes: [...routes, ...mountedE2eRoutes],
66
+ routes: [createLoginMethodsRoute({loginMethods}), ...routes, ...mountedE2eRoutes],
66
67
  fastifyOptions: {trustProxy: parseApiTrustProxy(config.API_TRUST_PROXY)},
67
68
  });
68
69
 
@@ -116,10 +117,24 @@ export async function createServer(options: CreateServerOptions): Promise<Server
116
117
  () => workersHandle?.stop(),
117
118
  () => shutdownServiceMetrics(),
118
119
  () => closePostgresClient(),
119
- async () => {
120
- await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
121
- },
122
120
  ]);
121
+ for (const cleanupError of cleanupErrors) {
122
+ logger().error({err: cleanupError}, 'Failed to clean up API server during shutdown');
123
+ reportError(cleanupError, {boundary: 'api.shutdown', operation: 'cleanup'});
124
+ }
125
+ try {
126
+ const errorMonitoringClosed = await closeErrorMonitoring(
127
+ ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS,
128
+ );
129
+ if (!errorMonitoringClosed) {
130
+ logger().error('Timed out closing error monitoring during API shutdown');
131
+ cleanupErrors.push(
132
+ new Error('Timed out closing error monitoring during API shutdown'),
133
+ );
134
+ }
135
+ } catch (error) {
136
+ cleanupErrors.push(error);
137
+ }
123
138
  throwCleanupErrors(cleanupErrors, 'Failed to stop API server');
124
139
  stopped = true;
125
140
  hasActiveServer = false;
@@ -138,6 +153,7 @@ export async function createServer(options: CreateServerOptions): Promise<Server
138
153
  ]);
139
154
  for (const cleanupError of cleanupErrors) {
140
155
  logger().error({err: cleanupError, bootError: error}, 'Failed to clean up API server boot');
156
+ reportError(cleanupError, {boundary: 'api.startup', operation: 'cleanup'});
141
157
  }
142
158
  hasActiveServer = false;
143
159
  throw error;
@@ -168,11 +184,23 @@ export async function runServer(options: RunServerOptions): Promise<ServerHandle
168
184
  {err: cleanupError, startError: error},
169
185
  'Failed to clean up API server startup',
170
186
  );
187
+ reportError(cleanupError, {
188
+ boundary: 'api.startup',
189
+ operation: 'cleanup-after-failed-start',
190
+ });
171
191
  }
172
192
  throw error;
173
193
  }
174
194
 
175
- const stopAndExit = () => void handle.stop().finally(() => process.exit(0));
195
+ const stopAndExit = () =>
196
+ void handle.stop().then(
197
+ () => process.exit(0),
198
+ (error) => {
199
+ logger().error({err: error}, 'Failed to stop API server after shutdown signal');
200
+ reportError(error, {boundary: 'api.shutdown', operation: 'signal-stop'});
201
+ process.exit(1);
202
+ },
203
+ );
176
204
  process.once('SIGTERM', stopAndExit);
177
205
  process.once('SIGINT', stopAndExit);
178
206
 
@@ -210,7 +238,9 @@ async function runCleanupSteps(steps: CleanupStep[]): Promise<unknown[]> {
210
238
  function throwCleanupErrors(cleanupErrors: unknown[], message: string): void {
211
239
  if (cleanupErrors.length === 0) return;
212
240
  if (cleanupErrors.length === 1) throw cleanupErrors[0];
213
- throw new AggregateError(cleanupErrors, message);
241
+ const aggregate = new AggregateError(cleanupErrors, message);
242
+ markErrorReported(aggregate);
243
+ throw aggregate;
214
244
  }
215
245
 
216
246
  async function handleModuleWorkerFailure(
@@ -246,11 +276,19 @@ async function handleModuleRuntimeFailure(options: {
246
276
  onFailure(): void | Promise<void>;
247
277
  }): Promise<void> {
248
278
  logger().error({err: options.error, ...options.fields}, options.message);
249
- captureException(options.error);
279
+ reportError(options.error, {
280
+ boundary: 'api.runtime',
281
+ tags: options.fields,
282
+ });
250
283
 
251
284
  try {
252
285
  await closeHttpServerAfterRuntimeFailure();
253
- await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
286
+ const errorMonitoringClosed = await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
287
+ if (!errorMonitoringClosed) {
288
+ logger().error('Timed out closing error monitoring after module runtime failure');
289
+ }
290
+ } catch (error) {
291
+ logger().error({err: error}, 'Failed to close error monitoring after module runtime failure');
254
292
  } finally {
255
293
  await options.onFailure();
256
294
  }
@@ -270,13 +308,13 @@ async function closeHttpServerAfterRuntimeFailure(): Promise<void> {
270
308
  },
271
309
  );
272
310
  if (result === 'timeout') {
273
- logger().error(
274
- {timeoutMs: RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS},
275
- 'Timed out closing HTTP server after module runtime failure',
276
- );
311
+ const error = new Error('Timed out closing HTTP server after module runtime failure');
312
+ logger().error({timeoutMs: RUNTIME_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS}, error.message);
313
+ reportError(error, {boundary: 'api.runtime', operation: 'close-http-timeout'});
277
314
  }
278
315
  } catch (error) {
279
316
  logger().error({err: error}, 'Failed to close HTTP server after module runtime failure');
317
+ reportError(error, {boundary: 'api.runtime', operation: 'close-http'});
280
318
  } finally {
281
319
  if (timeoutId) {
282
320
  clearTimeout(timeoutId);