@newrelic/preflight 1.18.1 → 1.18.3
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/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/dashboard-server.d.ts +8 -0
- package/dist/dashboard/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-server.js +33 -0
- package/dist/dashboard/dashboard-server.js.map +1 -1
- package/dist/dashboard/routes/api-handler.d.ts +4 -0
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +5 -0
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/homelab/homelab-accumulator.d.ts +21 -0
- package/dist/homelab/homelab-accumulator.d.ts.map +1 -0
- package/dist/homelab/homelab-accumulator.js +139 -0
- package/dist/homelab/homelab-accumulator.js.map +1 -0
- package/dist/homelab/homelab-forwarder.d.ts +24 -0
- package/dist/homelab/homelab-forwarder.d.ts.map +1 -0
- package/dist/homelab/homelab-forwarder.js +96 -0
- package/dist/homelab/homelab-forwarder.js.map +1 -0
- package/dist/homelab/index.d.ts +5 -0
- package/dist/homelab/index.d.ts.map +1 -0
- package/dist/homelab/index.js +3 -0
- package/dist/homelab/index.js.map +1 -0
- package/dist/hooks/collector-script.d.ts.map +1 -1
- package/dist/hooks/collector-script.js +29 -0
- package/dist/hooks/collector-script.js.map +1 -1
- package/dist/hooks/event-processor.d.ts +18 -0
- package/dist/hooks/event-processor.d.ts.map +1 -1
- package/dist/hooks/event-processor.js +28 -0
- package/dist/hooks/event-processor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +143 -8
- package/dist/index.js.map +1 -1
- package/dist/install/install-helper.d.ts +5 -0
- package/dist/install/install-helper.d.ts.map +1 -1
- package/dist/install/install-helper.js +16 -3
- package/dist/install/install-helper.js.map +1 -1
- package/dist/metrics/api-failure-tracker.d.ts +29 -0
- package/dist/metrics/api-failure-tracker.d.ts.map +1 -1
- package/dist/metrics/api-failure-tracker.js +56 -3
- package/dist/metrics/api-failure-tracker.js.map +1 -1
- package/dist/security/ssrf.d.ts +15 -2
- package/dist/security/ssrf.d.ts.map +1 -1
- package/dist/security/ssrf.js +25 -7
- package/dist/security/ssrf.js.map +1 -1
- package/dist/storage/types.d.ts +17 -1
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/tools/extended-analytics-tools.js +1 -1
- package/dist/tools/extended-analytics-tools.js.map +1 -1
- package/dist/transport/nr-ingest.d.ts +4 -0
- package/dist/transport/nr-ingest.d.ts.map +1 -1
- package/dist/transport/nr-ingest.js +7 -1
- package/dist/transport/nr-ingest.js.map +1 -1
- package/dist/web/assets/index-BBZyx02c.js +64 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-CC_l0esP.js +0 -64
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { Command, Option } from 'commander';
|
|
4
4
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
5
6
|
import { resolve } from 'node:path';
|
|
6
7
|
import { AlertLog } from './alerts/alert-log.js';
|
|
7
8
|
import { AlertSnapshotCollector } from './alerts/alert-snapshot-collector.js';
|
|
@@ -24,7 +25,7 @@ import { migrateStoragePath } from './install/migrate.js';
|
|
|
24
25
|
import { checkNodeVersion } from './install/node-version-check.js';
|
|
25
26
|
import { localDateKey, todayPortionOfSessionCost } from './lib/date.js';
|
|
26
27
|
import { AntiPatternDetector } from './metrics/anti-patterns.js';
|
|
27
|
-
import { ApiFailureTracker } from './metrics/api-failure-tracker.js';
|
|
28
|
+
import { ApiFailureTracker, mapClaudeCodeErrorType } from './metrics/api-failure-tracker.js';
|
|
28
29
|
import { BudgetTracker } from './metrics/budget-tracker.js';
|
|
29
30
|
import { ClaudeMdTracker } from './metrics/claudemd-tracker.js';
|
|
30
31
|
import { CollaborationProfiler } from './metrics/collaboration-profile.js';
|
|
@@ -75,6 +76,7 @@ import { SessionSpan } from './tracing/session-span.js';
|
|
|
75
76
|
import { TaskSpanTracker } from './tracing/task-span-tracker.js';
|
|
76
77
|
import { emitToolCallSpan } from './tracing/tool-call-span.js';
|
|
77
78
|
import { NrIngestManager } from './transport/nr-ingest.js';
|
|
79
|
+
import { HomelabAccumulator, HomelabForwarder } from './homelab/index.js';
|
|
78
80
|
import { VERSION } from './version.js';
|
|
79
81
|
export { loadMcpConfig, redactSensitive } from './config.js';
|
|
80
82
|
export { AmazonQAdapter, ClaudeCodeAdapter, ContinueAdapter, CopilotAdapter, createDefaultRegistry, CursorAdapter, GenericMcpAdapter, parseCopilotUsageResponse, PlatformRegistry, REPORT_SESSION_END_TOOL, REPORT_SESSION_START_TOOL, REPORT_TOOL_CALL_TOOL, validateReportToolCallInput, WindsurfAdapter, ZedAdapter, } from './platforms/index.js';
|
|
@@ -324,6 +326,7 @@ export function startDashboardRepoll(opts) {
|
|
|
324
326
|
const SUBCOMMAND_NAMES = [
|
|
325
327
|
'deploy-dashboards',
|
|
326
328
|
'deploy-alerts',
|
|
329
|
+
'server',
|
|
327
330
|
'install',
|
|
328
331
|
'uninstall',
|
|
329
332
|
'setup',
|
|
@@ -335,8 +338,8 @@ const SUBCOMMAND_NAMES = [
|
|
|
335
338
|
];
|
|
336
339
|
// Install-CLI subcommands — a subset of SUBCOMMAND_NAMES routed to runInstallCli.
|
|
337
340
|
// Derived from SUBCOMMAND_NAMES to ensure a single source of truth: deploy-*
|
|
338
|
-
// commands are excluded and handled by the commander path in dispatchSubcommand.
|
|
339
|
-
const INSTALL_CLI_SUBCOMMANDS = SUBCOMMAND_NAMES.filter((s) => s !== 'deploy-dashboards' && s !== 'deploy-alerts');
|
|
341
|
+
// and server commands are excluded and handled by the commander path in dispatchSubcommand.
|
|
342
|
+
const INSTALL_CLI_SUBCOMMANDS = SUBCOMMAND_NAMES.filter((s) => s !== 'deploy-dashboards' && s !== 'deploy-alerts' && s !== 'server');
|
|
340
343
|
function isSubcommand(value) {
|
|
341
344
|
return typeof value === 'string' && SUBCOMMAND_NAMES.includes(value);
|
|
342
345
|
}
|
|
@@ -392,6 +395,76 @@ export async function dispatchSubcommand(argv) {
|
|
|
392
395
|
process.exitCode = code;
|
|
393
396
|
});
|
|
394
397
|
}
|
|
398
|
+
else if (sub === 'server') {
|
|
399
|
+
program
|
|
400
|
+
.command('server')
|
|
401
|
+
.description('Start Preflight in homelab server mode — receives events forwarded from remote clients and accumulates them to disk (no dashboard yet — see docs/homelab.md)')
|
|
402
|
+
.option('--port <port>', 'Port to bind to (default: 7777 or NEW_RELIC_AI_HOMELAB_SERVER_PORT)')
|
|
403
|
+
.option('--token <token>', 'Bearer token for /ingest auth (default: NEW_RELIC_AI_HOMELAB_TOKEN)')
|
|
404
|
+
.option('--bind <address>', 'Bind address (default: 0.0.0.0 or NEW_RELIC_AI_HOMELAB_BIND_ADDRESS)')
|
|
405
|
+
.option('--config <path>', 'Path to config file')
|
|
406
|
+
.action(async (opts) => {
|
|
407
|
+
if (opts.port)
|
|
408
|
+
process.env.NEW_RELIC_AI_HOMELAB_SERVER_PORT = opts.port;
|
|
409
|
+
if (opts.token)
|
|
410
|
+
process.env.NEW_RELIC_AI_HOMELAB_TOKEN = opts.token;
|
|
411
|
+
if (opts.bind)
|
|
412
|
+
process.env.NEW_RELIC_AI_HOMELAB_BIND_ADDRESS = opts.bind;
|
|
413
|
+
// server mode never needs cloud credentials — force local so loadMcpConfig
|
|
414
|
+
// doesn't require licenseKey/accountId.
|
|
415
|
+
process.env.NR_AI_MODE = 'local';
|
|
416
|
+
const config = loadConfigOrDie({ config: opts.config });
|
|
417
|
+
const token = config.homelabToken;
|
|
418
|
+
if (!token) {
|
|
419
|
+
process.stderr.write('[preflight] error: homelab server requires a token. Set --token or NEW_RELIC_AI_HOMELAB_TOKEN.\n');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
const accumulator = new HomelabAccumulator({ storagePath: config.storagePath });
|
|
423
|
+
accumulator.start();
|
|
424
|
+
const bus = new LiveEventBus();
|
|
425
|
+
const dashboardServer = new DashboardServer({
|
|
426
|
+
port: config.homelabServer.port,
|
|
427
|
+
host: config.homelabServer.bindAddress,
|
|
428
|
+
bus,
|
|
429
|
+
serverMode: true,
|
|
430
|
+
ingestHandler: (authHeader, body) => {
|
|
431
|
+
const expected = `Bearer ${token}`;
|
|
432
|
+
const isAuthed = authHeader !== undefined &&
|
|
433
|
+
authHeader.length === expected.length &&
|
|
434
|
+
timingSafeEqual(Buffer.from(authHeader), Buffer.from(expected));
|
|
435
|
+
if (!isAuthed)
|
|
436
|
+
return 401;
|
|
437
|
+
const result = accumulator.handleIngest(body);
|
|
438
|
+
return result === 'ok' ? 204 : 400;
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
await dashboardServer.start();
|
|
442
|
+
logger.info('Homelab server ready', {
|
|
443
|
+
port: config.homelabServer.port,
|
|
444
|
+
bind: config.homelabServer.bindAddress,
|
|
445
|
+
});
|
|
446
|
+
// Keep the action (and therefore program.parseAsync) alive until a
|
|
447
|
+
// signal fires — without this, the action returns immediately and
|
|
448
|
+
// dispatchSubcommand exits the process.
|
|
449
|
+
await new Promise((resolve) => {
|
|
450
|
+
let shutting = false;
|
|
451
|
+
const shutdown = async () => {
|
|
452
|
+
if (shutting)
|
|
453
|
+
return;
|
|
454
|
+
shutting = true;
|
|
455
|
+
accumulator.stop();
|
|
456
|
+
await dashboardServer.stop();
|
|
457
|
+
resolve();
|
|
458
|
+
};
|
|
459
|
+
process.on('SIGTERM', () => {
|
|
460
|
+
void shutdown();
|
|
461
|
+
});
|
|
462
|
+
process.on('SIGINT', () => {
|
|
463
|
+
void shutdown();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
395
468
|
else {
|
|
396
469
|
program
|
|
397
470
|
.command('deploy-alerts')
|
|
@@ -582,6 +655,7 @@ async function main() {
|
|
|
582
655
|
}, getPendingConfirmationCapMs());
|
|
583
656
|
pendingConfirmationCapTimer.unref?.();
|
|
584
657
|
};
|
|
658
|
+
let homelabForwarder = null;
|
|
585
659
|
let shuttingDown = false;
|
|
586
660
|
const shutdown = async () => {
|
|
587
661
|
if (shuttingDown)
|
|
@@ -652,6 +726,7 @@ async function main() {
|
|
|
652
726
|
nrIngest ? nrIngest.stop() : Promise.resolve(),
|
|
653
727
|
mcpServer ? mcpServer.close() : Promise.resolve(),
|
|
654
728
|
proxyManager ? proxyManager.stop() : Promise.resolve(),
|
|
729
|
+
homelabForwarder ? homelabForwarder.stop() : Promise.resolve(),
|
|
655
730
|
]);
|
|
656
731
|
for (const r of stopResults) {
|
|
657
732
|
if (r.status === 'rejected') {
|
|
@@ -818,11 +893,12 @@ async function main() {
|
|
|
818
893
|
});
|
|
819
894
|
const toolSelectionScorer = new ToolSelectionScorer();
|
|
820
895
|
const qualityProxyTracker = new QualityProxyTracker();
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
896
|
+
// Fed via Claude Code's StopFailure hook (see the onApiFailure callback on
|
|
897
|
+
// eventProcessor below) through recordFailure(). recordRequest() still has
|
|
898
|
+
// no caller — request-level latency/throughput stays unobservable — and
|
|
899
|
+
// recordFailure()'s tokensInFlight/recoveryMs/retryCount/totalRequests
|
|
900
|
+
// inputs stay at their "unknown" defaults for the same reason. See
|
|
901
|
+
// api-failure-tracker.ts's API_FAILURE_PARTIAL_DATA_NOTE for the full story.
|
|
826
902
|
const apiFailureTracker = new ApiFailureTracker();
|
|
827
903
|
liveSessionRegistry = new LiveSessionRegistry();
|
|
828
904
|
liveSessionRegistry.startSampling();
|
|
@@ -1350,6 +1426,7 @@ async function main() {
|
|
|
1350
1426
|
},
|
|
1351
1427
|
antiPatternDetector,
|
|
1352
1428
|
retryDetector,
|
|
1429
|
+
apiFailureTracker,
|
|
1353
1430
|
instructionDriftTracker,
|
|
1354
1431
|
decisionTracker,
|
|
1355
1432
|
turnCostAttributor,
|
|
@@ -1541,11 +1618,34 @@ async function main() {
|
|
|
1541
1618
|
costTracker,
|
|
1542
1619
|
efficiencyScorer,
|
|
1543
1620
|
feedbackCollector,
|
|
1621
|
+
apiFailureTracker,
|
|
1544
1622
|
turnCostAttributor,
|
|
1545
1623
|
sessionTraceId,
|
|
1546
1624
|
});
|
|
1547
1625
|
capturedNrIngest = nrIngest;
|
|
1548
1626
|
}
|
|
1627
|
+
// Start homelab forwarder if configured. Construction validates
|
|
1628
|
+
// homelabServerUrl (scheme + cloud-metadata-endpoint check) and can
|
|
1629
|
+
// throw — caught here so a bad/malicious URL only disables forwarding,
|
|
1630
|
+
// matching the "forwarding failures are non-fatal" contract in
|
|
1631
|
+
// docs/homelab.md, rather than crashing MCP startup.
|
|
1632
|
+
if (config.homelabServerUrl && config.homelabToken) {
|
|
1633
|
+
try {
|
|
1634
|
+
homelabForwarder = new HomelabForwarder({
|
|
1635
|
+
serverUrl: config.homelabServerUrl,
|
|
1636
|
+
token: config.homelabToken,
|
|
1637
|
+
developer: config.developer,
|
|
1638
|
+
sessionId: sessionTraceId ?? 'unknown',
|
|
1639
|
+
});
|
|
1640
|
+
homelabForwarder.start();
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
homelabForwarder = null;
|
|
1644
|
+
logger.error('Homelab forwarder failed to start — continuing without it', {
|
|
1645
|
+
error: String(err),
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1549
1649
|
const capturedAlertEngine = alertEngine;
|
|
1550
1650
|
const capturedAlertSnapshotCollector = alertSnapshotCollector;
|
|
1551
1651
|
budgetTracker.setOnThreshold((event) => {
|
|
@@ -1774,6 +1874,7 @@ async function main() {
|
|
|
1774
1874
|
});
|
|
1775
1875
|
}
|
|
1776
1876
|
}
|
|
1877
|
+
homelabForwarder?.enqueue(record);
|
|
1777
1878
|
},
|
|
1778
1879
|
onTokenEvent: (tokenEvent) => {
|
|
1779
1880
|
if (!costTracker || !config)
|
|
@@ -1939,6 +2040,31 @@ async function main() {
|
|
|
1939
2040
|
: {}),
|
|
1940
2041
|
});
|
|
1941
2042
|
},
|
|
2043
|
+
onApiFailure: (frame) => {
|
|
2044
|
+
if (!config)
|
|
2045
|
+
return;
|
|
2046
|
+
const errorType = mapClaudeCodeErrorType(frame.rawErrorType);
|
|
2047
|
+
// Prefer a model already learned from real token events over the
|
|
2048
|
+
// config default, mirroring the estimateModel fallback above.
|
|
2049
|
+
const model = costTracker.getMetrics().model ?? config.model ?? 'unknown';
|
|
2050
|
+
const turnNumber = turnTracker.getCurrentTurnNumber();
|
|
2051
|
+
// totalTurnsInSession is left unspecified: there is no reliable
|
|
2052
|
+
// "total turns for this session" figure available at failure time
|
|
2053
|
+
// (the session may still be ongoing), and recordFailure() already
|
|
2054
|
+
// defaults it sensibly for sessionPhase bucketing.
|
|
2055
|
+
const duringToolExecution = eventProcessor !== undefined && eventProcessor.pendingCount > 0;
|
|
2056
|
+
apiFailureTracker.recordFailure({
|
|
2057
|
+
errorType,
|
|
2058
|
+
model,
|
|
2059
|
+
turnNumber,
|
|
2060
|
+
// Unobservable from StopFailure; see api-failure-tracker.ts's note constant.
|
|
2061
|
+
tokensInFlight: 0,
|
|
2062
|
+
// Known, not a placeholder: StopFailure only fires after Claude
|
|
2063
|
+
// Code's own retries are exhausted with no successful recovery.
|
|
2064
|
+
recoverySucceeded: false,
|
|
2065
|
+
duringToolExecution,
|
|
2066
|
+
});
|
|
2067
|
+
},
|
|
1942
2068
|
});
|
|
1943
2069
|
persistSession = (opts) => {
|
|
1944
2070
|
if (!sessionStore || !sessionTracker || !taskDetector || !config)
|
|
@@ -2305,6 +2431,7 @@ async function main() {
|
|
|
2305
2431
|
costTracker,
|
|
2306
2432
|
efficiencyScorer,
|
|
2307
2433
|
feedbackCollector,
|
|
2434
|
+
apiFailureTracker,
|
|
2308
2435
|
turnCostAttributor,
|
|
2309
2436
|
sessionTraceId: realId,
|
|
2310
2437
|
});
|
|
@@ -2606,6 +2733,13 @@ async function main() {
|
|
|
2606
2733
|
// proxied traffic instead of silently living in memory only.
|
|
2607
2734
|
const proxyLocalStore = new LocalStore(config.storagePath);
|
|
2608
2735
|
proxyLocalStore.initialize();
|
|
2736
|
+
// Proxy mode never constructs a HookEventProcessor, so nothing ever
|
|
2737
|
+
// calls recordFailure() on this instance (StopFailure is a Claude
|
|
2738
|
+
// Code-side hook, not something proxied traffic can surface) — passed
|
|
2739
|
+
// in anyway so NrIngestManager's emitMetrics() call is uniform across
|
|
2740
|
+
// modes, legitimately reporting zero failures rather than being asked
|
|
2741
|
+
// to handle a missing tracker.
|
|
2742
|
+
const apiFailureTracker = new ApiFailureTracker();
|
|
2609
2743
|
nrIngest = new NrIngestManager({
|
|
2610
2744
|
licenseKey: config.licenseKey,
|
|
2611
2745
|
transportOptions: {
|
|
@@ -2621,6 +2755,7 @@ async function main() {
|
|
|
2621
2755
|
localStore: proxyLocalStore,
|
|
2622
2756
|
eventHarvestIntervalMs: config.harvestIntervalMs.events,
|
|
2623
2757
|
metricHarvestIntervalMs: config.harvestIntervalMs.metrics,
|
|
2758
|
+
apiFailureTracker,
|
|
2624
2759
|
sessionTraceId,
|
|
2625
2760
|
// Proxy mode has no single coherent session — sessionTracker above is
|
|
2626
2761
|
// never fed per-client activity, so the ai.session.* gauges would
|