autotel 2.25.4 → 2.26.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 (69) hide show
  1. package/README.md +47 -1
  2. package/dist/auto.cjs +2 -2
  3. package/dist/auto.js +1 -1
  4. package/dist/{chunk-OPTGXEVN.js → chunk-4PTCDOZY.js} +3 -3
  5. package/dist/{chunk-OPTGXEVN.js.map → chunk-4PTCDOZY.js.map} +1 -1
  6. package/dist/{chunk-MN6PZ4AN.cjs → chunk-CMADDTHY.cjs} +7 -7
  7. package/dist/{chunk-MN6PZ4AN.cjs.map → chunk-CMADDTHY.cjs.map} +1 -1
  8. package/dist/{chunk-CMUM4JQI.js → chunk-DGPUZ6TE.js} +3 -3
  9. package/dist/{chunk-CMUM4JQI.js.map → chunk-DGPUZ6TE.js.map} +1 -1
  10. package/dist/{chunk-MNBAXRVG.js → chunk-EXOXDI5A.js} +74 -4
  11. package/dist/chunk-EXOXDI5A.js.map +1 -0
  12. package/dist/{chunk-QDREXAD7.js → chunk-GTD3NXOS.js} +3 -3
  13. package/dist/{chunk-QDREXAD7.js.map → chunk-GTD3NXOS.js.map} +1 -1
  14. package/dist/{chunk-A5ZUL2RZ.cjs → chunk-II7GFVAF.cjs} +13 -13
  15. package/dist/{chunk-A5ZUL2RZ.cjs.map → chunk-II7GFVAF.cjs.map} +1 -1
  16. package/dist/{chunk-I6JPSD4R.cjs → chunk-N344PVE5.cjs} +5 -5
  17. package/dist/{chunk-I6JPSD4R.cjs.map → chunk-N344PVE5.cjs.map} +1 -1
  18. package/dist/{chunk-WYP6OOCT.js → chunk-RXFZKLRQ.js} +3 -3
  19. package/dist/{chunk-WYP6OOCT.js.map → chunk-RXFZKLRQ.js.map} +1 -1
  20. package/dist/{chunk-EEJGUBWV.cjs → chunk-TS7IHIRW.cjs} +5 -5
  21. package/dist/{chunk-EEJGUBWV.cjs.map → chunk-TS7IHIRW.cjs.map} +1 -1
  22. package/dist/{chunk-ITYASFHQ.cjs → chunk-UJJPTSEI.cjs} +74 -3
  23. package/dist/chunk-UJJPTSEI.cjs.map +1 -0
  24. package/dist/{chunk-XB2GITM5.js → chunk-WAB4CHBU.js} +3 -3
  25. package/dist/{chunk-XB2GITM5.js.map → chunk-WAB4CHBU.js.map} +1 -1
  26. package/dist/{chunk-QQLP4M6W.cjs → chunk-ZJ5GXCOT.cjs} +26 -26
  27. package/dist/{chunk-QQLP4M6W.cjs.map → chunk-ZJ5GXCOT.cjs.map} +1 -1
  28. package/dist/decorators.cjs +2 -2
  29. package/dist/decorators.js +2 -2
  30. package/dist/event.cjs +5 -5
  31. package/dist/event.js +2 -2
  32. package/dist/functional.cjs +9 -9
  33. package/dist/functional.js +2 -2
  34. package/dist/index.cjs +42 -41
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +10 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/{init-C_PiC_Su.d.ts → init-FiR_glVc.d.ts} +23 -0
  41. package/dist/{init-CIzpC5kZ.d.cts → init-QSj7X6zU.d.cts} +23 -0
  42. package/dist/instrumentation.cjs +8 -8
  43. package/dist/instrumentation.js +1 -1
  44. package/dist/messaging.cjs +6 -6
  45. package/dist/messaging.js +3 -3
  46. package/dist/semantic-helpers.cjs +7 -7
  47. package/dist/semantic-helpers.js +3 -3
  48. package/dist/test-span-collector.cjs.map +1 -1
  49. package/dist/test-span-collector.d.cts +5 -2
  50. package/dist/test-span-collector.d.ts +5 -2
  51. package/dist/test-span-collector.js.map +1 -1
  52. package/dist/webhook.cjs +3 -3
  53. package/dist/webhook.js +2 -2
  54. package/dist/workflow-distributed.cjs +4 -4
  55. package/dist/workflow-distributed.js +2 -2
  56. package/dist/workflow.cjs +7 -7
  57. package/dist/workflow.js +3 -3
  58. package/dist/yaml-config.d.cts +1 -1
  59. package/dist/yaml-config.d.ts +1 -1
  60. package/package.json +41 -41
  61. package/src/devtools.ts +60 -0
  62. package/src/hook.mjs +2 -2
  63. package/src/init.customization.test.ts +81 -0
  64. package/src/init.ts +71 -1
  65. package/src/shutdown.test.ts +35 -1
  66. package/src/shutdown.ts +3 -1
  67. package/src/test-span-collector.ts +5 -2
  68. package/dist/chunk-ITYASFHQ.cjs.map +0 -1
  69. package/dist/chunk-MNBAXRVG.js.map +0 -1
package/dist/workflow.js CHANGED
@@ -1,10 +1,10 @@
1
- export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-WYP6OOCT.js';
2
- import './chunk-OPTGXEVN.js';
1
+ export { getCurrentWorkflowContext, isInWorkflow, traceStep, traceWorkflow } from './chunk-RXFZKLRQ.js';
2
+ import './chunk-4PTCDOZY.js';
3
3
  import './chunk-B3ZHLLMP.js';
4
4
  import './chunk-WD4RP6IV.js';
5
5
  import './chunk-S4OFEXLA.js';
6
6
  import './chunk-BBBWDIYQ.js';
7
- import './chunk-MNBAXRVG.js';
7
+ import './chunk-EXOXDI5A.js';
8
8
  import './chunk-RUD7KS4R.js';
9
9
  import './chunk-XDKK53OL.js';
10
10
  import './chunk-WGWSHJ2N.js';
@@ -1,4 +1,4 @@
1
- import { A as AutotelConfig } from './init-CIzpC5kZ.cjs';
1
+ import { A as AutotelConfig } from './init-QSj7X6zU.cjs';
2
2
  import { SamplingPreset } from './sampling.cjs';
3
3
  import '@opentelemetry/sdk-trace-base';
4
4
  import '@opentelemetry/sdk-node';
@@ -1,4 +1,4 @@
1
- import { A as AutotelConfig } from './init-C_PiC_Su.js';
1
+ import { A as AutotelConfig } from './init-FiR_glVc.js';
2
2
  import { SamplingPreset } from './sampling.js';
3
3
  import '@opentelemetry/sdk-trace-base';
4
4
  import '@opentelemetry/sdk-node';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel",
3
- "version": "2.25.4",
3
+ "version": "2.26.0",
4
4
  "description": "Write Once, Observe Anywhere",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -249,31 +249,31 @@
249
249
  "author": "Jag Reehal<jag@jagreehal.com> (https://jagreehal.com)",
250
250
  "license": "MIT",
251
251
  "dependencies": {
252
- "@opentelemetry/api": "^1.9.0",
253
- "@opentelemetry/api-logs": "^0.213.0",
254
- "@opentelemetry/exporter-logs-otlp-http": "^0.213.0",
255
- "@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
256
- "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
257
- "@opentelemetry/instrumentation": "^0.213.0",
258
- "@opentelemetry/resources": "^2.6.0",
259
- "@opentelemetry/sdk-logs": "^0.213.0",
260
- "@opentelemetry/sdk-metrics": "^2.6.0",
261
- "@opentelemetry/sdk-node": "^0.213.0",
262
- "@opentelemetry/sdk-trace-base": "^2.6.0",
252
+ "@opentelemetry/api": "^1.9.1",
253
+ "@opentelemetry/api-logs": "^0.214.0",
254
+ "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
255
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
256
+ "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
257
+ "@opentelemetry/instrumentation": "^0.214.0",
258
+ "@opentelemetry/resources": "^2.6.1",
259
+ "@opentelemetry/sdk-logs": "^0.214.0",
260
+ "@opentelemetry/sdk-metrics": "^2.6.1",
261
+ "@opentelemetry/sdk-node": "^0.214.0",
262
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
263
263
  "@opentelemetry/semantic-conventions": "^1.40.0",
264
- "@tanstack/intent": "^0.0.23",
265
- "import-in-the-middle": "^3.0.0"
264
+ "import-in-the-middle": "^3.0.1",
265
+ "@tanstack/intent": "^0.0.29"
266
266
  },
267
267
  "peerDependencies": {
268
- "@opentelemetry/auto-instrumentations-node": "^0.71.0",
269
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
270
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
271
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
272
- "@opentelemetry/resource-detector-aws": "^2.13.0",
273
- "@opentelemetry/resource-detector-container": "^0.8.4",
274
- "@opentelemetry/resource-detector-gcp": "^0.48.0",
275
- "@opentelemetry/sdk-trace-node": "^2.6.0",
276
- "@traceloop/node-server-sdk": "^0.22.8",
268
+ "@opentelemetry/auto-instrumentations-node": "^0.72.0",
269
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
270
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0",
271
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
272
+ "@opentelemetry/resource-detector-aws": "^2.14.0",
273
+ "@opentelemetry/resource-detector-container": "^0.8.5",
274
+ "@opentelemetry/resource-detector-gcp": "^0.49.0",
275
+ "@opentelemetry/sdk-trace-node": "^2.6.1",
276
+ "@traceloop/node-server-sdk": "^0.24.0",
277
277
  "pino": "^10.3.1",
278
278
  "pino-pretty": "^13.1.3",
279
279
  "yaml": "^2.8.3"
@@ -319,35 +319,35 @@
319
319
  "devDependencies": {
320
320
  "@arethetypeswrong/cli": "^0.18.2",
321
321
  "@edge-runtime/vm": "^5.0.0",
322
- "@opentelemetry/auto-instrumentations-node": "^0.71.0",
323
- "@opentelemetry/context-async-hooks": "^2.6.0",
324
- "@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
325
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
326
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.213.0",
327
- "@opentelemetry/resource-detector-aws": "^2.13.0",
328
- "@opentelemetry/resource-detector-container": "^0.8.4",
329
- "@opentelemetry/resource-detector-gcp": "^0.48.0",
330
- "@opentelemetry/sdk-trace-node": "^2.6.0",
331
- "@swc/core": "^1.15.21",
322
+ "@opentelemetry/auto-instrumentations-node": "^0.72.0",
323
+ "@opentelemetry/context-async-hooks": "^2.6.1",
324
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
325
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0",
326
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
327
+ "@opentelemetry/resource-detector-aws": "^2.14.0",
328
+ "@opentelemetry/resource-detector-container": "^0.8.5",
329
+ "@opentelemetry/resource-detector-gcp": "^0.49.0",
330
+ "@opentelemetry/sdk-trace-node": "^2.6.1",
331
+ "@swc/core": "^1.15.24",
332
332
  "@total-typescript/ts-reset": "^0.6.1",
333
333
  "@total-typescript/tsconfig": "^1.0.4",
334
334
  "@types/eslint-config-prettier": "^6.11.3",
335
- "@types/node": "^25.5.0",
336
- "@typescript-eslint/eslint-plugin": "^8.57.1",
337
- "@typescript-eslint/parser": "^8.57.1",
335
+ "@types/node": "^25.5.2",
336
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
337
+ "@typescript-eslint/parser": "^8.58.1",
338
338
  "eslint-config-prettier": "^10.1.8",
339
- "eslint-plugin-unicorn": "^63.0.0",
339
+ "eslint-plugin-unicorn": "^64.0.0",
340
340
  "pino": "^10.3.1",
341
341
  "prettier": "^3.8.1",
342
342
  "rimraf": "^6.1.3",
343
343
  "tsup": "^8.5.1",
344
344
  "tsx": "^4.21.0",
345
- "typescript": "^5.9.3",
346
- "typescript-eslint": "^8.57.1",
345
+ "typescript": "^6.0.2",
346
+ "typescript-eslint": "^8.58.1",
347
347
  "unplugin-swc": "^1.5.9",
348
348
  "vite-tsconfig-paths": "^6.1.1",
349
- "vitest": "^4.1.0",
350
- "vitest-mock-extended": "^3.1.0",
349
+ "vitest": "^4.1.3",
350
+ "vitest-mock-extended": "^4.0.0",
351
351
  "winston": "^3.19.0",
352
352
  "yaml": "^2.8.3"
353
353
  },
@@ -0,0 +1,60 @@
1
+ export interface AutotelDevtoolsConfig {
2
+ enabled?: boolean;
3
+ endpoint?: string;
4
+ embedded?: boolean;
5
+ host?: string;
6
+ port?: number;
7
+ verbose?: boolean;
8
+ }
9
+
10
+ export interface ResolvedAutotelDevtoolsConfig {
11
+ enabled: boolean;
12
+ endpoint?: string;
13
+ embedded: boolean;
14
+ host: string;
15
+ port: number;
16
+ verbose: boolean;
17
+ }
18
+
19
+ const defaultHost = '127.0.0.1';
20
+ const defaultPort = 4318;
21
+
22
+ export function resolveDevtoolsConfig(
23
+ config: boolean | AutotelDevtoolsConfig | undefined,
24
+ ): ResolvedAutotelDevtoolsConfig {
25
+ if (!config) {
26
+ return {
27
+ enabled: false,
28
+ endpoint: undefined,
29
+ embedded: false,
30
+ host: defaultHost,
31
+ port: defaultPort,
32
+ verbose: false,
33
+ };
34
+ }
35
+
36
+ if (config === true) {
37
+ return {
38
+ enabled: true,
39
+ endpoint: `http://${defaultHost}:${defaultPort}`,
40
+ embedded: false,
41
+ host: defaultHost,
42
+ port: defaultPort,
43
+ verbose: false,
44
+ };
45
+ }
46
+
47
+ const enabled = config.enabled ?? true;
48
+ const host = config.host ?? defaultHost;
49
+ const port = config.port ?? defaultPort;
50
+ const endpoint = config.endpoint ?? `http://${host}:${port}`;
51
+
52
+ return {
53
+ enabled,
54
+ endpoint: enabled ? endpoint : undefined,
55
+ embedded: enabled && (config.embedded ?? false),
56
+ host,
57
+ port,
58
+ verbose: config.verbose ?? false,
59
+ };
60
+ }
package/src/hook.mjs CHANGED
@@ -4,10 +4,10 @@
4
4
  * This file re-exports the OpenTelemetry ESM loader hook so users don't need
5
5
  * to install @opentelemetry/instrumentation as a direct dependency.
6
6
  *
7
- * Usage (Node 18+):
7
+ * Usage (Node 22+):
8
8
  * NODE_OPTIONS="--experimental-loader=autotel/hook.mjs --import ./instrumentation.ts" tsx src/index.ts
9
9
  *
10
- * For Node 20.6+, prefer using autotel/register instead which uses the newer
10
+ * For supported Node versions, prefer using autotel/register instead which uses the newer
11
11
  * module.register() API and doesn't require NODE_OPTIONS.
12
12
  *
13
13
  * @see https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/esm-support.md
@@ -115,6 +115,9 @@ async function loadInitWithMocks() {
115
115
  getConfig: mod.getConfig,
116
116
  getDefaultSampler: mod.getDefaultSampler,
117
117
  resolveLogsFlag: mod.resolveLogsFlag,
118
+ setOptionalRequireForTesting: mod._setOptionalRequireForTesting,
119
+ resetOptionalRequireForTesting: mod._resetOptionalRequireForTesting,
120
+ getEmbeddedDevtoolsCloseForTesting: mod._getEmbeddedDevtoolsCloseForTesting,
118
121
  sdkInstances,
119
122
  traceExporterOptions,
120
123
  metricExporterOptions,
@@ -135,6 +138,84 @@ describe('init() customization', () => {
135
138
  delete process.env.NODE_ENV;
136
139
  });
137
140
 
141
+ it('auto-configures local devtools endpoint and logs when devtools is enabled', async () => {
142
+ const {
143
+ init,
144
+ sdkInstances,
145
+ traceExporterOptions,
146
+ metricExporterOptions,
147
+ logExporterOptions,
148
+ } = await loadInitWithMocks();
149
+
150
+ init({ service: 'devtools-app', devtools: true });
151
+
152
+ expect(traceExporterOptions[0]).toMatchObject({
153
+ url: 'http://127.0.0.1:4318/v1/traces',
154
+ });
155
+ expect(metricExporterOptions[0]).toMatchObject({
156
+ url: 'http://127.0.0.1:4318/v1/metrics',
157
+ });
158
+ expect(logExporterOptions[0]).toMatchObject({
159
+ url: 'http://127.0.0.1:4318/v1/logs',
160
+ });
161
+
162
+ const options = sdkInstances.at(-1)?.options as Record<string, unknown>;
163
+ expect(options.logRecordProcessors).toBeDefined();
164
+ });
165
+
166
+ it('starts embedded autotel-devtools when requested and installed', async () => {
167
+ const {
168
+ init,
169
+ setOptionalRequireForTesting,
170
+ getEmbeddedDevtoolsCloseForTesting,
171
+ traceExporterOptions,
172
+ logExporterOptions,
173
+ } = await loadInitWithMocks();
174
+
175
+ const close = vi.fn();
176
+
177
+ setOptionalRequireForTesting((id: string) => {
178
+ if (id === 'autotel-devtools') {
179
+ return {
180
+ createDevtools: () => ({
181
+ port: 9876,
182
+ close,
183
+ }),
184
+ } as any;
185
+ }
186
+ return undefined;
187
+ });
188
+
189
+ init({
190
+ service: 'embedded-devtools-app',
191
+ devtools: { embedded: true, host: '127.0.0.1', port: 0 },
192
+ });
193
+
194
+ expect(traceExporterOptions[0]).toMatchObject({
195
+ url: 'http://127.0.0.1:9876/v1/traces',
196
+ });
197
+ expect(logExporterOptions[0]).toMatchObject({
198
+ url: 'http://127.0.0.1:9876/v1/logs',
199
+ });
200
+ expect(getEmbeddedDevtoolsCloseForTesting()).toBe(close);
201
+ });
202
+
203
+ it('falls back cleanly when embedded devtools is requested but unavailable', async () => {
204
+ const { init, setOptionalRequireForTesting, traceExporterOptions } =
205
+ await loadInitWithMocks();
206
+
207
+ setOptionalRequireForTesting(() => undefined);
208
+
209
+ init({
210
+ service: 'embedded-devtools-fallback-app',
211
+ devtools: { embedded: true },
212
+ });
213
+
214
+ expect(traceExporterOptions[0]).toMatchObject({
215
+ url: 'http://127.0.0.1:4318/v1/traces',
216
+ });
217
+ });
218
+
138
219
  it(
139
220
  'passes custom instrumentations to the NodeSDK',
140
221
  { timeout: 10_000 },
package/src/init.ts CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  type CanonicalLogLineOptions,
74
74
  } from './processors/canonical-log-line-processor';
75
75
  import type { EventsConfig } from './events-config';
76
+ import { resolveDevtoolsConfig, type AutotelDevtoolsConfig } from './devtools';
76
77
 
77
78
  /**
78
79
  * Silent logger (no-op) - used as default when user doesn't provide one.
@@ -297,6 +298,21 @@ export interface AutotelConfig {
297
298
  /** Service name (required) */
298
299
  service: string;
299
300
 
301
+ /**
302
+ * Local developer UX for autotel-devtools.
303
+ *
304
+ * - `true`: send traces, metrics, and logs to `http://127.0.0.1:4318`
305
+ * - `{ embedded: true }`: attempt to start `autotel-devtools` automatically
306
+ *
307
+ * When enabled:
308
+ * - `endpoint` defaults to the local devtools URL
309
+ * - `logs` default to `true` unless explicitly set
310
+ *
311
+ * This keeps production config unchanged while making local debugging
312
+ * effectively zero-config.
313
+ */
314
+ devtools?: boolean | AutotelDevtoolsConfig;
315
+
300
316
  /** Event subscribers - bring your own (PostHog, Mixpanel, etc.) */
301
317
  subscribers?: EventSubscriber[];
302
318
 
@@ -1138,6 +1154,7 @@ let validationConfig: Partial<ValidationConfig> | null = null;
1138
1154
  let eventsConfig: EventsConfig | null = null;
1139
1155
  let _stringRedactor: StringRedactor | null = null;
1140
1156
  let _optionalRequire: typeof safeRequire = safeRequire;
1157
+ let _devtoolsClose: (() => Promise<void> | void) | null = null;
1141
1158
 
1142
1159
  /**
1143
1160
  * Resolve metrics flag with env var override support
@@ -1297,6 +1314,11 @@ export function init(cfg: AutotelConfig): void {
1297
1314
  headers: cfg.headers ?? yamlConfig.headers ?? envConfig.headers,
1298
1315
  } as AutotelConfig;
1299
1316
 
1317
+ const devtoolsConfig = resolveDevtoolsConfig(mergedConfig.devtools);
1318
+ if (devtoolsConfig.enabled && mergedConfig.logs === undefined) {
1319
+ mergedConfig.logs = true;
1320
+ }
1321
+
1300
1322
  // Set logger (use provided or default to silent - no spam)
1301
1323
  logger = mergedConfig.logger || silentLogger;
1302
1324
 
@@ -1314,7 +1336,7 @@ export function init(cfg: AutotelConfig): void {
1314
1336
 
1315
1337
  // Initialize OpenTelemetry
1316
1338
  // Only use endpoint if explicitly configured (no default fallback)
1317
- const endpoint = mergedConfig.endpoint;
1339
+ let endpoint = mergedConfig.endpoint ?? devtoolsConfig.endpoint;
1318
1340
  const otlpHeaders = normalizeOtlpHeaders(mergedConfig.headers);
1319
1341
  const version = mergedConfig.version || detectVersion();
1320
1342
  const environment =
@@ -1322,6 +1344,35 @@ export function init(cfg: AutotelConfig): void {
1322
1344
  const metricsEnabled = resolveMetricsFlag(mergedConfig.metrics);
1323
1345
  const logsEnabled = resolveLogsFlag(mergedConfig.logs);
1324
1346
 
1347
+ if (devtoolsConfig.enabled && devtoolsConfig.embedded) {
1348
+ const devtoolsModule = _optionalRequire<{
1349
+ createDevtools?: (options?: {
1350
+ port?: number;
1351
+ host?: string;
1352
+ verbose?: boolean;
1353
+ }) => { port: number; close: () => Promise<void> | void };
1354
+ }>('autotel-devtools');
1355
+
1356
+ if (devtoolsModule?.createDevtools) {
1357
+ const devtoolsInstance = devtoolsModule.createDevtools({
1358
+ port: devtoolsConfig.port,
1359
+ host: devtoolsConfig.host,
1360
+ verbose: devtoolsConfig.verbose,
1361
+ });
1362
+ _devtoolsClose = devtoolsInstance.close;
1363
+ endpoint = `http://${devtoolsConfig.host}:${devtoolsInstance.port}`;
1364
+ logger.info(
1365
+ {},
1366
+ `[autotel] autotel-devtools embedded server started at ${endpoint}`,
1367
+ );
1368
+ } else {
1369
+ logger.warn(
1370
+ {},
1371
+ '[autotel] devtools.embedded requested but autotel-devtools is not installed. Falling back to endpoint-only mode.',
1372
+ );
1373
+ }
1374
+ }
1375
+
1325
1376
  // Detect hostname for proper Datadog correlation and Service Catalog discovery
1326
1377
  const hostname = detectHostname();
1327
1378
 
@@ -2013,6 +2064,25 @@ export function _resetOptionalRequireForTesting(): void {
2013
2064
  _optionalRequire = safeRequire;
2014
2065
  }
2015
2066
 
2067
+ /**
2068
+ * @internal Close embedded devtools if running.
2069
+ */
2070
+ export async function _closeEmbeddedDevtools(): Promise<void> {
2071
+ if (_devtoolsClose) {
2072
+ await _devtoolsClose();
2073
+ _devtoolsClose = null;
2074
+ }
2075
+ }
2076
+
2077
+ /**
2078
+ * @internal Get embedded devtools close handle.
2079
+ */
2080
+ export function _getEmbeddedDevtoolsCloseForTesting():
2081
+ | (() => Promise<void> | void)
2082
+ | null {
2083
+ return _devtoolsClose;
2084
+ }
2085
+
2016
2086
  /**
2017
2087
  * Get SDK instance (for shutdown)
2018
2088
  */
@@ -4,7 +4,12 @@
4
4
 
5
5
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
6
  import { flush, shutdown } from './shutdown';
7
- import { init } from './init';
7
+ import {
8
+ init,
9
+ _getEmbeddedDevtoolsCloseForTesting,
10
+ _resetOptionalRequireForTesting,
11
+ _setOptionalRequireForTesting,
12
+ } from './init';
8
13
  import { track, getEventQueue } from './track';
9
14
  import { EventSubscriber } from './event-subscriber';
10
15
 
@@ -37,6 +42,7 @@ describe('shutdown module', () => {
37
42
  beforeEach(() => {
38
43
  vi.clearAllMocks();
39
44
  mockAdapter = new MockAdapter();
45
+ _resetOptionalRequireForTesting();
40
46
  });
41
47
 
42
48
  afterEach(async () => {
@@ -45,6 +51,7 @@ describe('shutdown module', () => {
45
51
  if (queue) {
46
52
  await queue.flush();
47
53
  }
54
+ _resetOptionalRequireForTesting();
48
55
  });
49
56
 
50
57
  describe('flush()', () => {
@@ -288,6 +295,33 @@ describe('shutdown module', () => {
288
295
  // Should not throw even if adapter shutdown fails
289
296
  await expect(shutdown()).resolves.toBeUndefined();
290
297
  });
298
+
299
+ it('should close embedded devtools during shutdown', async () => {
300
+ const close = vi.fn().mockResolvedValue(undefined);
301
+ _setOptionalRequireForTesting((id: string) => {
302
+ if (id === 'autotel-devtools') {
303
+ return {
304
+ createDevtools: () => ({
305
+ port: 4318,
306
+ close,
307
+ }),
308
+ } as any;
309
+ }
310
+ return undefined;
311
+ });
312
+
313
+ init({
314
+ service: 'test-service',
315
+ devtools: { embedded: true },
316
+ });
317
+
318
+ expect(_getEmbeddedDevtoolsCloseForTesting()).not.toBeNull();
319
+
320
+ await shutdown();
321
+
322
+ expect(close).toHaveBeenCalledOnce();
323
+ expect(_getEmbeddedDevtoolsCloseForTesting()).toBeNull();
324
+ });
291
325
  });
292
326
 
293
327
  describe('Integration', () => {
package/src/shutdown.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Graceful shutdown with flush and cleanup
3
3
  */
4
4
 
5
- import { getSdk, getLogger } from './init';
5
+ import { getSdk, getLogger, _closeEmbeddedDevtools } from './init';
6
6
  import { getEventQueue, resetEventQueue } from './track';
7
7
  import { resetEvents } from './event';
8
8
  import { resetMetrics } from './metric';
@@ -179,6 +179,8 @@ export async function shutdown(): Promise<void> {
179
179
  logger.error({ err }, '[autotel] SDK shutdown failed');
180
180
  }
181
181
  } finally {
182
+ await _closeEmbeddedDevtools();
183
+
182
184
  // Clean up singleton Maps and queues to prevent memory leaks
183
185
  // This runs even if SDK shutdown fails
184
186
  const eventsQueue = getEventQueue();
@@ -36,8 +36,11 @@ type SerializableValue =
36
36
  /**
37
37
  * Portable serialized span for embedding in test metadata.
38
38
  * `startTimeMs` is derived from OTel HrTime — epoch-based wall-clock ms in the current SDK.
39
+ *
40
+ * Defined as a `type` (not `interface`) so it is assignable to
41
+ * `Record<string, unknown>` in TypeScript 6+ strict mode.
39
42
  */
40
- export interface SerializedSpan {
43
+ export type SerializedSpan = {
41
44
  spanId: string;
42
45
  parentSpanId?: string;
43
46
  name: string;
@@ -46,7 +49,7 @@ export interface SerializedSpan {
46
49
  status: 'ok' | 'error' | 'unset';
47
50
  statusMessage?: string;
48
51
  attributes?: Record<string, SerializableValue>;
49
- }
52
+ };
50
53
 
51
54
  export class TestSpanCollector implements SpanExporter {
52
55
  private traces = new Map<string, ReadableSpan[]>();