@neat.is/core 0.7.9 → 0.8.0-rc.1
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/{chunk-N5TPODCX.js → chunk-3PXRQH53.js} +905 -37
- package/dist/chunk-3PXRQH53.js.map +1 -0
- package/dist/{chunk-ILG3SMD5.js → chunk-OJ6BW63H.js} +6 -2
- package/dist/chunk-OJ6BW63H.js.map +1 -0
- package/dist/cli.cjs +1673 -270
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +689 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +922 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +922 -42
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +911 -39
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ILG3SMD5.js.map +0 -1
- package/dist/chunk-N5TPODCX.js.map +0 -1
package/dist/server.cjs
CHANGED
|
@@ -755,7 +755,7 @@ function getGraph(project = DEFAULT_PROJECT) {
|
|
|
755
755
|
init_cjs_shims();
|
|
756
756
|
var import_fastify2 = __toESM(require("fastify"), 1);
|
|
757
757
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
758
|
-
var
|
|
758
|
+
var import_types85 = require("@neat.is/types");
|
|
759
759
|
|
|
760
760
|
// src/extend/index.ts
|
|
761
761
|
init_cjs_shims();
|
|
@@ -2017,6 +2017,7 @@ var import_yaml = require("yaml");
|
|
|
2017
2017
|
var import_types3 = require("@neat.is/types");
|
|
2018
2018
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
2019
2019
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2020
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
2020
2021
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2021
2022
|
"node_modules",
|
|
2022
2023
|
".git",
|
|
@@ -2056,6 +2057,7 @@ async function isPythonVenvDir(dir) {
|
|
|
2056
2057
|
function isConfigFile(name) {
|
|
2057
2058
|
const ext = import_node_path5.default.extname(name);
|
|
2058
2059
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2060
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
2059
2061
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2060
2062
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2061
2063
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -4321,6 +4323,63 @@ async function addRoutes(graph, services) {
|
|
|
4321
4323
|
return { nodesAdded, edgesAdded };
|
|
4322
4324
|
}
|
|
4323
4325
|
|
|
4326
|
+
// src/latency-digest.ts
|
|
4327
|
+
init_cjs_shims();
|
|
4328
|
+
var SUB = 16;
|
|
4329
|
+
var MIN_EXP = -10;
|
|
4330
|
+
var MAX_EXP = 22;
|
|
4331
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4332
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4333
|
+
function latencyBucketIndex(ms) {
|
|
4334
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4335
|
+
const e = Math.floor(Math.log2(ms));
|
|
4336
|
+
if (e < MIN_EXP) return 0;
|
|
4337
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4338
|
+
const base = 2 ** e;
|
|
4339
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4340
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4341
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4342
|
+
}
|
|
4343
|
+
function bucketRepresentativeMs(index) {
|
|
4344
|
+
if (index <= 0) return 0;
|
|
4345
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4346
|
+
const zeroBased = index - 1;
|
|
4347
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4348
|
+
const s = zeroBased % SUB;
|
|
4349
|
+
const base = 2 ** e;
|
|
4350
|
+
const lower = base * (1 + s / SUB);
|
|
4351
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4352
|
+
return (lower + upper) / 2;
|
|
4353
|
+
}
|
|
4354
|
+
function recordLatency(hist, ms) {
|
|
4355
|
+
const key = String(latencyBucketIndex(ms));
|
|
4356
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4357
|
+
return hist;
|
|
4358
|
+
}
|
|
4359
|
+
function quantile(hist, q) {
|
|
4360
|
+
const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
|
|
4361
|
+
let total = 0;
|
|
4362
|
+
for (const [, c] of entries) total += c;
|
|
4363
|
+
if (total === 0) return 0;
|
|
4364
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4365
|
+
let cumulative = 0;
|
|
4366
|
+
for (const [idx, c] of entries) {
|
|
4367
|
+
cumulative += c;
|
|
4368
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4369
|
+
}
|
|
4370
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4371
|
+
}
|
|
4372
|
+
function round(ms) {
|
|
4373
|
+
return Math.round(ms * 100) / 100;
|
|
4374
|
+
}
|
|
4375
|
+
function latencyPercentiles(hist) {
|
|
4376
|
+
if (!hist) return void 0;
|
|
4377
|
+
let total = 0;
|
|
4378
|
+
for (const c of Object.values(hist)) total += c;
|
|
4379
|
+
if (total === 0) return void 0;
|
|
4380
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4324
4383
|
// src/ingest.ts
|
|
4325
4384
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4326
4385
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5016,7 +5075,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5016
5075
|
graph.addNode(id, node);
|
|
5017
5076
|
return id;
|
|
5018
5077
|
}
|
|
5019
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5078
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5020
5079
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5021
5080
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5022
5081
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5024,10 +5083,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5024
5083
|
const existing = graph.getEdgeAttributes(id);
|
|
5025
5084
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5026
5085
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5086
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5087
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5027
5088
|
const newSignal = {
|
|
5028
5089
|
spanCount: newSpanCount,
|
|
5029
5090
|
errorCount: newErrorCount,
|
|
5030
|
-
lastObservedAgeMs: 0
|
|
5091
|
+
lastObservedAgeMs: 0,
|
|
5092
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5093
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5094
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5031
5095
|
};
|
|
5032
5096
|
const updated = {
|
|
5033
5097
|
...existing,
|
|
@@ -5042,10 +5106,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5042
5106
|
graph.replaceEdgeAttributes(id, updated);
|
|
5043
5107
|
return { edge: updated, created: false };
|
|
5044
5108
|
}
|
|
5109
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5110
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5045
5111
|
const signal = {
|
|
5046
5112
|
spanCount: 1,
|
|
5047
5113
|
errorCount: isError ? 1 : 0,
|
|
5048
|
-
lastObservedAgeMs: 0
|
|
5114
|
+
lastObservedAgeMs: 0,
|
|
5115
|
+
...latencyHist ? { latencyHist } : {},
|
|
5116
|
+
...latencyMs ? { latencyMs } : {}
|
|
5049
5117
|
};
|
|
5050
5118
|
const edge = {
|
|
5051
5119
|
id,
|
|
@@ -5109,6 +5177,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
5109
5177
|
await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(ctx.errorsPath), { recursive: true });
|
|
5110
5178
|
await import_node_fs9.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5111
5179
|
}
|
|
5180
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
5181
|
+
const ev = {
|
|
5182
|
+
id: input.id,
|
|
5183
|
+
timestamp: input.timestamp,
|
|
5184
|
+
service: input.service,
|
|
5185
|
+
traceId: input.id,
|
|
5186
|
+
spanId: input.id,
|
|
5187
|
+
errorType: input.errorType,
|
|
5188
|
+
errorMessage: input.errorMessage,
|
|
5189
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
5190
|
+
affectedNode: input.affectedNode
|
|
5191
|
+
};
|
|
5192
|
+
await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(errorsPath), { recursive: true });
|
|
5193
|
+
await import_node_fs9.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5194
|
+
}
|
|
5112
5195
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
5113
5196
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types7.serviceId)(span.service, span.env);
|
|
5114
5197
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -5230,6 +5313,7 @@ async function handleSpan(ctx, span) {
|
|
|
5230
5313
|
}
|
|
5231
5314
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5232
5315
|
const isError = span.statusCode === 2;
|
|
5316
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5233
5317
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5234
5318
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5235
5319
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5268,7 +5352,8 @@ async function handleSpan(ctx, span) {
|
|
|
5268
5352
|
targetId,
|
|
5269
5353
|
ts,
|
|
5270
5354
|
isError,
|
|
5271
|
-
callSiteEvidence
|
|
5355
|
+
callSiteEvidence,
|
|
5356
|
+
durationMs
|
|
5272
5357
|
);
|
|
5273
5358
|
if (result) affectedNode = targetId;
|
|
5274
5359
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5280,7 +5365,8 @@ async function handleSpan(ctx, span) {
|
|
|
5280
5365
|
collectionId,
|
|
5281
5366
|
ts,
|
|
5282
5367
|
isError,
|
|
5283
|
-
callSiteEvidence
|
|
5368
|
+
callSiteEvidence,
|
|
5369
|
+
durationMs
|
|
5284
5370
|
);
|
|
5285
5371
|
}
|
|
5286
5372
|
if (span.dbTable) {
|
|
@@ -5292,7 +5378,8 @@ async function handleSpan(ctx, span) {
|
|
|
5292
5378
|
tableId,
|
|
5293
5379
|
ts,
|
|
5294
5380
|
isError,
|
|
5295
|
-
callSiteEvidence
|
|
5381
|
+
callSiteEvidence,
|
|
5382
|
+
durationMs
|
|
5296
5383
|
);
|
|
5297
5384
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5298
5385
|
}
|
|
@@ -5311,7 +5398,8 @@ async function handleSpan(ctx, span) {
|
|
|
5311
5398
|
targetId,
|
|
5312
5399
|
ts,
|
|
5313
5400
|
isError,
|
|
5314
|
-
callSiteEvidence
|
|
5401
|
+
callSiteEvidence,
|
|
5402
|
+
durationMs
|
|
5315
5403
|
);
|
|
5316
5404
|
if (result) affectedNode = targetId;
|
|
5317
5405
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5328,7 +5416,8 @@ async function handleSpan(ctx, span) {
|
|
|
5328
5416
|
targetId,
|
|
5329
5417
|
ts,
|
|
5330
5418
|
isError,
|
|
5331
|
-
callSiteEvidence
|
|
5419
|
+
callSiteEvidence,
|
|
5420
|
+
durationMs
|
|
5332
5421
|
);
|
|
5333
5422
|
if (result) affectedNode = targetId;
|
|
5334
5423
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5340,7 +5429,8 @@ async function handleSpan(ctx, span) {
|
|
|
5340
5429
|
targetId,
|
|
5341
5430
|
ts,
|
|
5342
5431
|
isError,
|
|
5343
|
-
callSiteEvidence
|
|
5432
|
+
callSiteEvidence,
|
|
5433
|
+
durationMs
|
|
5344
5434
|
);
|
|
5345
5435
|
if (result) affectedNode = targetId;
|
|
5346
5436
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5356,7 +5446,8 @@ async function handleSpan(ctx, span) {
|
|
|
5356
5446
|
targetId,
|
|
5357
5447
|
ts,
|
|
5358
5448
|
isError,
|
|
5359
|
-
callSiteEvidence
|
|
5449
|
+
callSiteEvidence,
|
|
5450
|
+
durationMs
|
|
5360
5451
|
);
|
|
5361
5452
|
if (result) affectedNode = targetId;
|
|
5362
5453
|
} else {
|
|
@@ -5372,7 +5463,8 @@ async function handleSpan(ctx, span) {
|
|
|
5372
5463
|
targetId,
|
|
5373
5464
|
ts,
|
|
5374
5465
|
isError,
|
|
5375
|
-
callSiteEvidence
|
|
5466
|
+
callSiteEvidence,
|
|
5467
|
+
durationMs
|
|
5376
5468
|
);
|
|
5377
5469
|
affectedNode = targetId;
|
|
5378
5470
|
resolvedViaAddress = true;
|
|
@@ -5385,7 +5477,8 @@ async function handleSpan(ctx, span) {
|
|
|
5385
5477
|
frontierNodeId,
|
|
5386
5478
|
ts,
|
|
5387
5479
|
isError,
|
|
5388
|
-
callSiteEvidence
|
|
5480
|
+
callSiteEvidence,
|
|
5481
|
+
durationMs
|
|
5389
5482
|
);
|
|
5390
5483
|
affectedNode = frontierNodeId;
|
|
5391
5484
|
resolvedViaAddress = true;
|
|
@@ -5411,7 +5504,8 @@ async function handleSpan(ctx, span) {
|
|
|
5411
5504
|
sourceId,
|
|
5412
5505
|
ts,
|
|
5413
5506
|
isError,
|
|
5414
|
-
fallbackEvidence
|
|
5507
|
+
fallbackEvidence,
|
|
5508
|
+
durationMs
|
|
5415
5509
|
);
|
|
5416
5510
|
}
|
|
5417
5511
|
}
|
|
@@ -5425,7 +5519,7 @@ async function handleSpan(ctx, span) {
|
|
|
5425
5519
|
);
|
|
5426
5520
|
if (routeNodeId) {
|
|
5427
5521
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5428
|
-
upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
5522
|
+
upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5429
5523
|
}
|
|
5430
5524
|
}
|
|
5431
5525
|
if (span.statusCode === 2) {
|
|
@@ -5925,7 +6019,7 @@ var rootCauseShapes = {
|
|
|
5925
6019
|
[import_types8.NodeType.FileNode]: fileRootCauseShape,
|
|
5926
6020
|
[import_types8.NodeType.SymbolNode]: symbolRootCauseShape
|
|
5927
6021
|
};
|
|
5928
|
-
function
|
|
6022
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
5929
6023
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
5930
6024
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
5931
6025
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -6182,7 +6276,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
6182
6276
|
dependencies: [],
|
|
6183
6277
|
observed: false,
|
|
6184
6278
|
inboundObservedCount: 0,
|
|
6185
|
-
hasExtractedOutbound: false
|
|
6279
|
+
hasExtractedOutbound: false,
|
|
6280
|
+
inboundVolume: 0,
|
|
6281
|
+
window: "lifetime"
|
|
6186
6282
|
});
|
|
6187
6283
|
}
|
|
6188
6284
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -6213,11 +6309,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
6213
6309
|
}
|
|
6214
6310
|
}
|
|
6215
6311
|
let inboundObservedCount = 0;
|
|
6312
|
+
let inboundVolume = 0;
|
|
6313
|
+
let inboundLastObserved;
|
|
6216
6314
|
for (const tgt of scope) {
|
|
6217
6315
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
6218
6316
|
const e = graph.getEdgeAttributes(edgeId);
|
|
6219
6317
|
if (e.type === import_types8.EdgeType.CONTAINS) continue;
|
|
6220
|
-
if (e.provenance === import_types8.Provenance.OBSERVED)
|
|
6318
|
+
if (e.provenance === import_types8.Provenance.OBSERVED) {
|
|
6319
|
+
inboundObservedCount += 1;
|
|
6320
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
6321
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
6322
|
+
inboundLastObserved = e.lastObserved;
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6221
6325
|
}
|
|
6222
6326
|
}
|
|
6223
6327
|
dependencies.sort(
|
|
@@ -6228,7 +6332,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
6228
6332
|
dependencies,
|
|
6229
6333
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
6230
6334
|
inboundObservedCount,
|
|
6231
|
-
hasExtractedOutbound
|
|
6335
|
+
hasExtractedOutbound,
|
|
6336
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
6337
|
+
inboundVolume,
|
|
6338
|
+
window: "lifetime",
|
|
6339
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
6340
|
+
});
|
|
6341
|
+
}
|
|
6342
|
+
var SATURATION_P95_MS = 1e3;
|
|
6343
|
+
function nodeScope(graph, nodeId) {
|
|
6344
|
+
const scope = [nodeId];
|
|
6345
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
6346
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
6347
|
+
if (attrs.type === import_types8.NodeType.ServiceNode) {
|
|
6348
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
6349
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
6350
|
+
if (e.type !== import_types8.EdgeType.CONTAINS) continue;
|
|
6351
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
6352
|
+
if (owned.type === import_types8.NodeType.FileNode) scope.push(e.target);
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
return scope;
|
|
6356
|
+
}
|
|
6357
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
6358
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
6359
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
6360
|
+
}
|
|
6361
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
6362
|
+
const scope = nodeScope(graph, nodeId);
|
|
6363
|
+
let errorsFromCallers = 0;
|
|
6364
|
+
let inboundVolume = 0;
|
|
6365
|
+
let outboundVolume = 0;
|
|
6366
|
+
let outboundErrors = 0;
|
|
6367
|
+
let latestInboundMs;
|
|
6368
|
+
let latencyP95Ms;
|
|
6369
|
+
let stale = false;
|
|
6370
|
+
for (const n of scope) {
|
|
6371
|
+
if (!graph.hasNode(n)) continue;
|
|
6372
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
6373
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
6374
|
+
if (e.type === import_types8.EdgeType.CONTAINS) continue;
|
|
6375
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
6376
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
6377
|
+
if (e.provenance === import_types8.Provenance.STALE) stale = true;
|
|
6378
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
6379
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
6380
|
+
if (e.lastObserved) {
|
|
6381
|
+
const t = Date.parse(e.lastObserved);
|
|
6382
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
6386
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
6387
|
+
if (e.type === import_types8.EdgeType.CONTAINS) continue;
|
|
6388
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
6389
|
+
if (e.type === import_types8.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
6390
|
+
if (e.provenance === import_types8.Provenance.STALE) stale = true;
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
6394
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
6395
|
+
return {
|
|
6396
|
+
errorsEmittedHere,
|
|
6397
|
+
errorsFromCallers,
|
|
6398
|
+
callCount: inboundVolume,
|
|
6399
|
+
outboundVolume,
|
|
6400
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
6401
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
6402
|
+
stale
|
|
6403
|
+
};
|
|
6404
|
+
}
|
|
6405
|
+
function isSaturated(ctx) {
|
|
6406
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
6407
|
+
}
|
|
6408
|
+
function classifyNode(ctx) {
|
|
6409
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
6410
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
6411
|
+
return "symptom-only";
|
|
6412
|
+
}
|
|
6413
|
+
return "primary-failure";
|
|
6414
|
+
}
|
|
6415
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
6416
|
+
return "unrelated";
|
|
6417
|
+
}
|
|
6418
|
+
function isVictimSeed(ctx) {
|
|
6419
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
6420
|
+
}
|
|
6421
|
+
function grainOf(graph, nodeId) {
|
|
6422
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
6423
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
6424
|
+
if (t === import_types8.NodeType.ServiceNode) return "service";
|
|
6425
|
+
if (t === import_types8.NodeType.FileNode) return "file";
|
|
6426
|
+
if (t === import_types8.NodeType.SymbolNode) return "symbol";
|
|
6427
|
+
return t;
|
|
6428
|
+
}
|
|
6429
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
6430
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
6431
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
6432
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
6433
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
6434
|
+
while (queue.length > 0) {
|
|
6435
|
+
const frame = queue.shift();
|
|
6436
|
+
if (frame.depth >= maxDepth) continue;
|
|
6437
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
6438
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
6439
|
+
for (const [nid, edge] of neighbours) {
|
|
6440
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
6441
|
+
if (enqueued.has(nid)) continue;
|
|
6442
|
+
enqueued.add(nid);
|
|
6443
|
+
queue.push({
|
|
6444
|
+
nodeId: nid,
|
|
6445
|
+
depth: frame.depth + 1,
|
|
6446
|
+
nodes: [...frame.nodes, nid],
|
|
6447
|
+
edges: [...frame.edges, edge]
|
|
6448
|
+
});
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
return null;
|
|
6452
|
+
}
|
|
6453
|
+
var EMPTY_CONTEXT = {
|
|
6454
|
+
errorsEmittedHere: 0,
|
|
6455
|
+
errorsFromCallers: 0,
|
|
6456
|
+
callCount: 0,
|
|
6457
|
+
outboundVolume: 0,
|
|
6458
|
+
stale: false
|
|
6459
|
+
};
|
|
6460
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
6461
|
+
if (!graph.hasNode(nodeId)) {
|
|
6462
|
+
return import_types8.ExpandResultSchema.parse({
|
|
6463
|
+
origin: nodeId,
|
|
6464
|
+
direction,
|
|
6465
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
6466
|
+
neighbours: []
|
|
6467
|
+
});
|
|
6468
|
+
}
|
|
6469
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
6470
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
6471
|
+
const neighbours = [];
|
|
6472
|
+
for (const [nid, edge] of best) {
|
|
6473
|
+
if (edge.type === import_types8.EdgeType.CONTAINS) continue;
|
|
6474
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
6475
|
+
neighbours.push({
|
|
6476
|
+
node: nid,
|
|
6477
|
+
edgeType: edge.type,
|
|
6478
|
+
provenance: edge.provenance,
|
|
6479
|
+
classification: classifyNode(nctx),
|
|
6480
|
+
context: nctx
|
|
6481
|
+
});
|
|
6482
|
+
}
|
|
6483
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
6484
|
+
return import_types8.ExpandResultSchema.parse({
|
|
6485
|
+
origin: nodeId,
|
|
6486
|
+
direction,
|
|
6487
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
6488
|
+
neighbours
|
|
6489
|
+
});
|
|
6490
|
+
}
|
|
6491
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
6492
|
+
const buildPath = (fp) => ({
|
|
6493
|
+
nodes: fp.nodes,
|
|
6494
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
6495
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
6496
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
6497
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
6498
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
6499
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
6500
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
6501
|
+
)
|
|
6502
|
+
});
|
|
6503
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
6504
|
+
return import_types8.RelateResultSchema.parse({
|
|
6505
|
+
a,
|
|
6506
|
+
b,
|
|
6507
|
+
related: false,
|
|
6508
|
+
direction: null,
|
|
6509
|
+
paths: [],
|
|
6510
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6513
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
6514
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
6515
|
+
if (!down && !up) {
|
|
6516
|
+
return import_types8.RelateResultSchema.parse({
|
|
6517
|
+
a,
|
|
6518
|
+
b,
|
|
6519
|
+
related: false,
|
|
6520
|
+
direction: null,
|
|
6521
|
+
paths: [],
|
|
6522
|
+
note: `no path within ${maxDepth} hops`
|
|
6523
|
+
});
|
|
6524
|
+
}
|
|
6525
|
+
const paths = [];
|
|
6526
|
+
const direction = down ? "a->b" : "b->a";
|
|
6527
|
+
if (down) paths.push(buildPath(down));
|
|
6528
|
+
if (up) paths.push(buildPath(up));
|
|
6529
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
6530
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
6531
|
+
return import_types8.RelateResultSchema.parse({
|
|
6532
|
+
a,
|
|
6533
|
+
b,
|
|
6534
|
+
related: true,
|
|
6535
|
+
direction,
|
|
6536
|
+
paths,
|
|
6537
|
+
...grainGap ? { grainGap: true } : {}
|
|
6538
|
+
});
|
|
6539
|
+
}
|
|
6540
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
6541
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
6542
|
+
let best = null;
|
|
6543
|
+
for (const nid of upstream) {
|
|
6544
|
+
if (nid === alertNodeId) continue;
|
|
6545
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
6546
|
+
if (ctx.outboundVolume === 0) continue;
|
|
6547
|
+
const isSource = ctx.callCount === 0;
|
|
6548
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
6549
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
6550
|
+
}
|
|
6551
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
6552
|
+
}
|
|
6553
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
6554
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
6555
|
+
if (!legacy) return null;
|
|
6556
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
6557
|
+
if (!navigation) return legacy;
|
|
6558
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
6559
|
+
}
|
|
6560
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
6561
|
+
const seedNode = legacy.rootCauseNode;
|
|
6562
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
6563
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
6564
|
+
const candidates = [];
|
|
6565
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
6566
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
6567
|
+
if (origin) {
|
|
6568
|
+
const path68 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
6569
|
+
const originConfidence = confidenceFromMix(path68?.edges ?? [], now);
|
|
6570
|
+
candidates.push({
|
|
6571
|
+
node: origin.node,
|
|
6572
|
+
classification: "primary-failure",
|
|
6573
|
+
reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
|
|
6574
|
+
context: origin.ctx,
|
|
6575
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
6576
|
+
provenance: import_types8.Provenance.OBSERVED
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
6580
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
6581
|
+
candidates.push({
|
|
6582
|
+
node: seedNode,
|
|
6583
|
+
classification: "symptom-only",
|
|
6584
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
6585
|
+
context: seedCtx,
|
|
6586
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
6587
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
6588
|
+
});
|
|
6589
|
+
} else {
|
|
6590
|
+
candidates.push({
|
|
6591
|
+
node: seedNode,
|
|
6592
|
+
classification: "primary-failure",
|
|
6593
|
+
reason: legacy.rootCauseReason,
|
|
6594
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
6595
|
+
confidence: legacy.confidence,
|
|
6596
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
const top = candidates[0];
|
|
6600
|
+
let traversalPath = legacy.traversalPath;
|
|
6601
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
6602
|
+
if (top.node !== seedNode) {
|
|
6603
|
+
const path68 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
6604
|
+
if (path68) {
|
|
6605
|
+
traversalPath = path68.nodes;
|
|
6606
|
+
edgeProvenances = path68.edges.map((e) => e.provenance);
|
|
6607
|
+
} else {
|
|
6608
|
+
traversalPath = [errorNodeId, top.node];
|
|
6609
|
+
edgeProvenances = [top.provenance ?? import_types8.Provenance.OBSERVED];
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
return import_types8.RootCauseResultSchema.parse({
|
|
6613
|
+
rootCauseNode: top.node,
|
|
6614
|
+
rootCauseReason: top.reason,
|
|
6615
|
+
traversalPath,
|
|
6616
|
+
edgeProvenances,
|
|
6617
|
+
confidence: top.confidence,
|
|
6618
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
6619
|
+
candidates
|
|
6232
6620
|
});
|
|
6233
6621
|
}
|
|
6234
6622
|
|
|
@@ -11801,8 +12189,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
|
|
|
11801
12189
|
var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
|
|
11802
12190
|
var import_types36 = require("@neat.is/types");
|
|
11803
12191
|
init_otel();
|
|
11804
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
11805
12192
|
var PARSE_CHUNK10 = 16384;
|
|
12193
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
12194
|
+
"Query",
|
|
12195
|
+
"QueryContext",
|
|
12196
|
+
"QueryRow",
|
|
12197
|
+
"QueryRowContext",
|
|
12198
|
+
"Exec",
|
|
12199
|
+
"ExecContext",
|
|
12200
|
+
"Prepare",
|
|
12201
|
+
"PrepareContext"
|
|
12202
|
+
]);
|
|
12203
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
12204
|
+
"Get",
|
|
12205
|
+
"Select",
|
|
12206
|
+
"Queryx",
|
|
12207
|
+
"QueryRowx",
|
|
12208
|
+
"NamedExec",
|
|
12209
|
+
"NamedQuery",
|
|
12210
|
+
"MustExec",
|
|
12211
|
+
"Preparex",
|
|
12212
|
+
"GetContext",
|
|
12213
|
+
"SelectContext"
|
|
12214
|
+
]);
|
|
12215
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
12216
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
12217
|
+
function makeGoParser3() {
|
|
12218
|
+
const p = new import_tree_sitter14.default();
|
|
12219
|
+
p.setLanguage(import_tree_sitter_go3.default);
|
|
12220
|
+
return p;
|
|
12221
|
+
}
|
|
12222
|
+
function parseSource10(parser, source) {
|
|
12223
|
+
return parser.parse(
|
|
12224
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
12225
|
+
);
|
|
12226
|
+
}
|
|
11806
12227
|
function walk7(node, visit) {
|
|
11807
12228
|
visit(node);
|
|
11808
12229
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -11810,25 +12231,54 @@ function walk7(node, visit) {
|
|
|
11810
12231
|
if (child) walk7(child, visit);
|
|
11811
12232
|
}
|
|
11812
12233
|
}
|
|
12234
|
+
function goStringLiteralValue(node) {
|
|
12235
|
+
if (!node) return null;
|
|
12236
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
12237
|
+
const t = node.text;
|
|
12238
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
12239
|
+
}
|
|
12240
|
+
return null;
|
|
12241
|
+
}
|
|
12242
|
+
function goImportsAny(root, names) {
|
|
12243
|
+
let found = false;
|
|
12244
|
+
walk7(root, (node) => {
|
|
12245
|
+
if (found || node.type !== "import_spec") return;
|
|
12246
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
12247
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
12248
|
+
if (value !== null && names.has(value)) found = true;
|
|
12249
|
+
}
|
|
12250
|
+
});
|
|
12251
|
+
return found;
|
|
12252
|
+
}
|
|
12253
|
+
function firstStringLiteralArg(argsNode) {
|
|
12254
|
+
if (!argsNode) return null;
|
|
12255
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
12256
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
12257
|
+
if (value !== null) return value;
|
|
12258
|
+
}
|
|
12259
|
+
return null;
|
|
12260
|
+
}
|
|
11813
12261
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
11814
12262
|
if (import_node_path49.default.extname(file.path) !== ".go") return [];
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
const
|
|
11818
|
-
|
|
11819
|
-
);
|
|
12263
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
12264
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
12265
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
12266
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
12267
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
11820
12268
|
const out = [];
|
|
11821
12269
|
walk7(tree.rootNode, (node) => {
|
|
11822
12270
|
if (node.type !== "call_expression") return;
|
|
11823
12271
|
const fn = node.childForFieldName("function");
|
|
11824
12272
|
if (fn?.type !== "selector_expression") return;
|
|
11825
12273
|
const method = fn.childForFieldName("field")?.text;
|
|
11826
|
-
if (!method
|
|
11827
|
-
const
|
|
11828
|
-
if (!
|
|
11829
|
-
const sql =
|
|
12274
|
+
if (!method) return;
|
|
12275
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
12276
|
+
if (!recognized) return;
|
|
12277
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
12278
|
+
if (sql === null) return;
|
|
11830
12279
|
const table = tableFromSqlStatement(sql);
|
|
11831
12280
|
if (!table) return;
|
|
12281
|
+
const columns = columnsFromSqlStatement(sql);
|
|
11832
12282
|
const line = node.startPosition.row + 1;
|
|
11833
12283
|
out.push({
|
|
11834
12284
|
infraId: (0, import_types36.infraId)("sql-table", table),
|
|
@@ -11836,7 +12286,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
11836
12286
|
kind: "sql-table",
|
|
11837
12287
|
edgeType: "CALLS",
|
|
11838
12288
|
confidenceKind: "verified-call-site",
|
|
11839
|
-
|
|
12289
|
+
...columns.length > 0 ? { columns } : {},
|
|
12290
|
+
evidence: {
|
|
12291
|
+
file: toPosix(import_node_path49.default.relative(serviceDir, file.path)),
|
|
12292
|
+
line,
|
|
12293
|
+
snippet: snippet(file.content, line)
|
|
12294
|
+
}
|
|
11840
12295
|
});
|
|
11841
12296
|
});
|
|
11842
12297
|
return out;
|
|
@@ -11850,12 +12305,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
|
|
|
11850
12305
|
var import_types37 = require("@neat.is/types");
|
|
11851
12306
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
11852
12307
|
var PARSE_CHUNK11 = 16384;
|
|
11853
|
-
function
|
|
12308
|
+
function makeGoParser4() {
|
|
11854
12309
|
const p = new import_tree_sitter15.default();
|
|
11855
12310
|
p.setLanguage(import_tree_sitter_go4.default);
|
|
11856
12311
|
return p;
|
|
11857
12312
|
}
|
|
11858
|
-
function
|
|
12313
|
+
function parseSource11(parser, source) {
|
|
11859
12314
|
return parser.parse(
|
|
11860
12315
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
11861
12316
|
);
|
|
@@ -12279,7 +12734,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
12279
12734
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
12280
12735
|
if (import_node_path50.default.extname(file.path) !== ".go") return [];
|
|
12281
12736
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
12282
|
-
const tree =
|
|
12737
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
12283
12738
|
const { structs, models, tableFor } = analyze(tree);
|
|
12284
12739
|
const out = [];
|
|
12285
12740
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -12310,7 +12765,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
12310
12765
|
function gormForeignKeys(file, serviceDir) {
|
|
12311
12766
|
if (import_node_path50.default.extname(file.path) !== ".go") return [];
|
|
12312
12767
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
12313
|
-
const tree =
|
|
12768
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
12314
12769
|
const { structs, models, tableFor } = analyze(tree);
|
|
12315
12770
|
const out = [];
|
|
12316
12771
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14593,6 +15048,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
14593
15048
|
unresolved++;
|
|
14594
15049
|
continue;
|
|
14595
15050
|
}
|
|
15051
|
+
if (signal.incident) {
|
|
15052
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
15053
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
15054
|
+
unresolved++;
|
|
15055
|
+
continue;
|
|
15056
|
+
}
|
|
15057
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
15058
|
+
id: signal.incident.id,
|
|
15059
|
+
timestamp: signal.incident.timestamp,
|
|
15060
|
+
service: signal.incident.service,
|
|
15061
|
+
errorType: signal.incident.errorType,
|
|
15062
|
+
errorMessage: signal.incident.errorMessage,
|
|
15063
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
15064
|
+
affectedNode: resolved.targetNodeId
|
|
15065
|
+
});
|
|
15066
|
+
continue;
|
|
15067
|
+
}
|
|
14596
15068
|
if (resolved.ensureInfraNode) {
|
|
14597
15069
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
14598
15070
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -16786,6 +17258,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
16786
17258
|
};
|
|
16787
17259
|
}
|
|
16788
17260
|
|
|
17261
|
+
// src/connectors/eas/index.ts
|
|
17262
|
+
init_cjs_shims();
|
|
17263
|
+
|
|
17264
|
+
// src/connectors/eas/client.ts
|
|
17265
|
+
init_cjs_shims();
|
|
17266
|
+
|
|
17267
|
+
// src/connectors/eas/types.ts
|
|
17268
|
+
init_cjs_shims();
|
|
17269
|
+
function readEasCredentials(raw) {
|
|
17270
|
+
const token = raw["token"];
|
|
17271
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
17272
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
17273
|
+
}
|
|
17274
|
+
return { token };
|
|
17275
|
+
}
|
|
17276
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
17277
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
17278
|
+
"SPIN_UP_BUILDER",
|
|
17279
|
+
"PREPARE_CREDENTIALS",
|
|
17280
|
+
"RESTORE_CACHE",
|
|
17281
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
17282
|
+
]);
|
|
17283
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
17284
|
+
function isTransientFailure(err) {
|
|
17285
|
+
if (!err) return false;
|
|
17286
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
17287
|
+
if (phase) {
|
|
17288
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
17289
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
17290
|
+
}
|
|
17291
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
17292
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
17293
|
+
return false;
|
|
17294
|
+
}
|
|
17295
|
+
var FIELD_SEP3 = "\0";
|
|
17296
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
17297
|
+
function packEasTargetName(identity) {
|
|
17298
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
17299
|
+
}
|
|
17300
|
+
function parseEasTargetName(targetName) {
|
|
17301
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
17302
|
+
if (sep === -1) return null;
|
|
17303
|
+
const serviceName = targetName.slice(0, sep);
|
|
17304
|
+
const phase = targetName.slice(sep + 1);
|
|
17305
|
+
if (!serviceName) return null;
|
|
17306
|
+
return { serviceName, phase };
|
|
17307
|
+
}
|
|
17308
|
+
|
|
17309
|
+
// src/connectors/eas/client.ts
|
|
17310
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
17311
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
17312
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
17313
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
17314
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
17315
|
+
var BUILDS_QUERY = `
|
|
17316
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
17317
|
+
app {
|
|
17318
|
+
byId(appId: $appId) {
|
|
17319
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
17320
|
+
id
|
|
17321
|
+
status
|
|
17322
|
+
platform
|
|
17323
|
+
buildProfile
|
|
17324
|
+
gitCommitHash
|
|
17325
|
+
gitCommitMessage
|
|
17326
|
+
gitRef
|
|
17327
|
+
isGitWorkingTreeDirty
|
|
17328
|
+
createdAt
|
|
17329
|
+
completedAt
|
|
17330
|
+
error {
|
|
17331
|
+
buildPhase
|
|
17332
|
+
errorCode
|
|
17333
|
+
message
|
|
17334
|
+
docsUrl
|
|
17335
|
+
}
|
|
17336
|
+
logFileUrls
|
|
17337
|
+
}
|
|
17338
|
+
}
|
|
17339
|
+
}
|
|
17340
|
+
}
|
|
17341
|
+
`;
|
|
17342
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
17343
|
+
const res = await junctionFetch(
|
|
17344
|
+
apiUrl,
|
|
17345
|
+
{
|
|
17346
|
+
method: "POST",
|
|
17347
|
+
headers: {
|
|
17348
|
+
"Content-Type": "application/json",
|
|
17349
|
+
...bearerAuthHeader(token)
|
|
17350
|
+
},
|
|
17351
|
+
body: JSON.stringify({ query, variables })
|
|
17352
|
+
},
|
|
17353
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
17354
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
17355
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
17356
|
+
);
|
|
17357
|
+
if (!res.ok) {
|
|
17358
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
17359
|
+
}
|
|
17360
|
+
const body = await res.json();
|
|
17361
|
+
if (body.errors && body.errors.length > 0) {
|
|
17362
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
17363
|
+
}
|
|
17364
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
17365
|
+
return body.data;
|
|
17366
|
+
}
|
|
17367
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
17368
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
17369
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
17370
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
17371
|
+
const out = [];
|
|
17372
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17373
|
+
for (let page = 0; page < maxPages; page++) {
|
|
17374
|
+
const data = await easGraphQL(
|
|
17375
|
+
apiUrl,
|
|
17376
|
+
token,
|
|
17377
|
+
BUILDS_QUERY,
|
|
17378
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
17379
|
+
config.appId,
|
|
17380
|
+
fetchImpl
|
|
17381
|
+
);
|
|
17382
|
+
const builds = data.app?.byId?.builds;
|
|
17383
|
+
if (!Array.isArray(builds)) break;
|
|
17384
|
+
let added = 0;
|
|
17385
|
+
for (const b of builds) {
|
|
17386
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
17387
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
17388
|
+
if (seen.has(b.id)) continue;
|
|
17389
|
+
seen.add(b.id);
|
|
17390
|
+
out.push(b);
|
|
17391
|
+
added++;
|
|
17392
|
+
}
|
|
17393
|
+
if (builds.length < pageSize) break;
|
|
17394
|
+
if (added === 0) break;
|
|
17395
|
+
}
|
|
17396
|
+
return out;
|
|
17397
|
+
}
|
|
17398
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
17399
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
17400
|
+
const doFetch = fetchImpl ?? fetch;
|
|
17401
|
+
const chunks = [];
|
|
17402
|
+
for (const url of logFileUrls) {
|
|
17403
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
17404
|
+
try {
|
|
17405
|
+
const res = await doFetch(url);
|
|
17406
|
+
if (!res.ok) continue;
|
|
17407
|
+
chunks.push(await res.text());
|
|
17408
|
+
} catch {
|
|
17409
|
+
}
|
|
17410
|
+
}
|
|
17411
|
+
const joined = chunks.join("\n");
|
|
17412
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
17413
|
+
}
|
|
17414
|
+
|
|
17415
|
+
// src/connectors/eas/map.ts
|
|
17416
|
+
init_cjs_shims();
|
|
17417
|
+
function buildEventTime(build) {
|
|
17418
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
17419
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
17420
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
17421
|
+
}
|
|
17422
|
+
function incidentMessage2(build) {
|
|
17423
|
+
const err = build.error ?? {};
|
|
17424
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
17425
|
+
const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
|
|
17426
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
17427
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
17428
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
17429
|
+
}
|
|
17430
|
+
return msg;
|
|
17431
|
+
}
|
|
17432
|
+
function incidentAttributes(build) {
|
|
17433
|
+
const attrs = {};
|
|
17434
|
+
const err = build.error ?? {};
|
|
17435
|
+
const put = (k, v) => {
|
|
17436
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
17437
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
17438
|
+
};
|
|
17439
|
+
put("eas.buildId", build.id);
|
|
17440
|
+
put("eas.platform", build.platform ?? void 0);
|
|
17441
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
17442
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
17443
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
17444
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
17445
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
17446
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
17447
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
17448
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
17449
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
17450
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
17451
|
+
}
|
|
17452
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
17453
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
17454
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
17455
|
+
attrs["eas.logs"] = build.logsText;
|
|
17456
|
+
}
|
|
17457
|
+
return attrs;
|
|
17458
|
+
}
|
|
17459
|
+
function mapBuildToSignal(build, serviceName) {
|
|
17460
|
+
if (!build || typeof build !== "object") return null;
|
|
17461
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
17462
|
+
if (!build.error) return null;
|
|
17463
|
+
if (isTransientFailure(build.error)) return null;
|
|
17464
|
+
const timestamp = buildEventTime(build);
|
|
17465
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
17466
|
+
return {
|
|
17467
|
+
targetKind: EAS_TARGET_KIND,
|
|
17468
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
17469
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
17470
|
+
callCount: 0,
|
|
17471
|
+
errorCount: 0,
|
|
17472
|
+
lastObservedIso: timestamp,
|
|
17473
|
+
incident: {
|
|
17474
|
+
id: `eas:build:${build.id}`,
|
|
17475
|
+
timestamp,
|
|
17476
|
+
service: serviceName,
|
|
17477
|
+
errorType: "eas-build-failure",
|
|
17478
|
+
errorMessage: incidentMessage2(build),
|
|
17479
|
+
attributes: incidentAttributes(build)
|
|
17480
|
+
}
|
|
17481
|
+
};
|
|
17482
|
+
}
|
|
17483
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
17484
|
+
const out = [];
|
|
17485
|
+
for (const build of builds) {
|
|
17486
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
17487
|
+
if (signal) out.push(signal);
|
|
17488
|
+
}
|
|
17489
|
+
return out;
|
|
17490
|
+
}
|
|
17491
|
+
|
|
17492
|
+
// src/connectors/eas/resolve.ts
|
|
17493
|
+
init_cjs_shims();
|
|
17494
|
+
var import_types82 = require("@neat.is/types");
|
|
17495
|
+
var NO_ENV2 = "unknown";
|
|
17496
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
17497
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
17498
|
+
"READ_APP_CONFIG",
|
|
17499
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
17500
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
17501
|
+
]);
|
|
17502
|
+
function configBasenamesForPhase(phase) {
|
|
17503
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
17504
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
17505
|
+
return [];
|
|
17506
|
+
}
|
|
17507
|
+
function configNodeService(graph, configNodeId) {
|
|
17508
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
17509
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
17510
|
+
if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
|
|
17511
|
+
const parsed = (0, import_types82.parseFileId)(edge.source);
|
|
17512
|
+
if (parsed) return parsed.service;
|
|
17513
|
+
}
|
|
17514
|
+
return null;
|
|
17515
|
+
}
|
|
17516
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
17517
|
+
let scoped = null;
|
|
17518
|
+
let anyMatch = null;
|
|
17519
|
+
graph.forEachNode((id, attrs) => {
|
|
17520
|
+
if (scoped) return;
|
|
17521
|
+
const node = attrs;
|
|
17522
|
+
if (node.type !== import_types82.NodeType.ConfigNode) return;
|
|
17523
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
17524
|
+
if (anyMatch === null) anyMatch = id;
|
|
17525
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
17526
|
+
});
|
|
17527
|
+
return scoped ?? anyMatch;
|
|
17528
|
+
}
|
|
17529
|
+
function createEasResolveTarget(graph) {
|
|
17530
|
+
return (signal) => {
|
|
17531
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
17532
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
17533
|
+
if (!identity) return null;
|
|
17534
|
+
const { serviceName, phase } = identity;
|
|
17535
|
+
const basenames = configBasenamesForPhase(phase);
|
|
17536
|
+
if (basenames.length > 0) {
|
|
17537
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
17538
|
+
if (configNodeId) {
|
|
17539
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
|
|
17540
|
+
}
|
|
17541
|
+
}
|
|
17542
|
+
return {
|
|
17543
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
17544
|
+
serviceName,
|
|
17545
|
+
edgeType: import_types82.EdgeType.CALLS
|
|
17546
|
+
};
|
|
17547
|
+
};
|
|
17548
|
+
}
|
|
17549
|
+
|
|
17550
|
+
// src/connectors/eas/index.ts
|
|
17551
|
+
function isBuildSince(build, sinceIso) {
|
|
17552
|
+
const t = Date.parse(buildEventTime(build));
|
|
17553
|
+
const s = Date.parse(sinceIso);
|
|
17554
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
17555
|
+
return t > s;
|
|
17556
|
+
}
|
|
17557
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
17558
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
17559
|
+
if (!since) return floor.toISOString();
|
|
17560
|
+
const sinceMs = new Date(since).getTime();
|
|
17561
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
17562
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
17563
|
+
}
|
|
17564
|
+
var EasConnector = class {
|
|
17565
|
+
constructor(config, fetchImpl) {
|
|
17566
|
+
this.config = config;
|
|
17567
|
+
this.fetchImpl = fetchImpl;
|
|
17568
|
+
}
|
|
17569
|
+
config;
|
|
17570
|
+
fetchImpl;
|
|
17571
|
+
provider = "eas";
|
|
17572
|
+
async poll(ctx) {
|
|
17573
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
17574
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
17575
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
17576
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
17577
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
17578
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
17579
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
17580
|
+
for (const build of fresh) {
|
|
17581
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
17582
|
+
}
|
|
17583
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
17584
|
+
}
|
|
17585
|
+
};
|
|
17586
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
17587
|
+
return {
|
|
17588
|
+
connector: new EasConnector(config, fetchImpl),
|
|
17589
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
17590
|
+
};
|
|
17591
|
+
}
|
|
17592
|
+
|
|
16789
17593
|
// src/connectors/registry.ts
|
|
16790
17594
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
16791
17595
|
async function authProbe(input) {
|
|
@@ -17073,6 +17877,41 @@ var PROVIDER_DISPATCH = {
|
|
|
17073
17877
|
...fetchImpl ? { fetchImpl } : {}
|
|
17074
17878
|
});
|
|
17075
17879
|
}
|
|
17880
|
+
},
|
|
17881
|
+
eas: {
|
|
17882
|
+
provider: "eas",
|
|
17883
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
17884
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
17885
|
+
primaryCredentialKey: "token",
|
|
17886
|
+
requiredCredentialFields: ["token"],
|
|
17887
|
+
requiredOptionFields: ["appId"],
|
|
17888
|
+
build(graph, options) {
|
|
17889
|
+
return createEasConnector(graph, options);
|
|
17890
|
+
},
|
|
17891
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
17892
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
17893
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
17894
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
17895
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
17896
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
17897
|
+
// silently at the first poll.
|
|
17898
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
17899
|
+
const cfg = options;
|
|
17900
|
+
const appId = String(cfg.appId ?? "");
|
|
17901
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
17902
|
+
const probeConfig = {
|
|
17903
|
+
appId,
|
|
17904
|
+
pageSize: 1,
|
|
17905
|
+
maxPages: 1,
|
|
17906
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
17907
|
+
};
|
|
17908
|
+
try {
|
|
17909
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
17910
|
+
return { ok: true };
|
|
17911
|
+
} catch (err) {
|
|
17912
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
17913
|
+
}
|
|
17914
|
+
}
|
|
17076
17915
|
}
|
|
17077
17916
|
};
|
|
17078
17917
|
function vercelCredsFrom(credentials) {
|
|
@@ -17382,11 +18221,11 @@ function registerRoutes(scope, ctx) {
|
|
|
17382
18221
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17383
18222
|
const parsed = [];
|
|
17384
18223
|
for (const c of candidates) {
|
|
17385
|
-
const r =
|
|
18224
|
+
const r = import_types85.DivergenceTypeSchema.safeParse(c);
|
|
17386
18225
|
if (!r.success) {
|
|
17387
18226
|
return reply.code(400).send({
|
|
17388
18227
|
error: `unknown divergence type "${c}"`,
|
|
17389
|
-
allowed:
|
|
18228
|
+
allowed: import_types85.DivergenceTypeSchema.options
|
|
17390
18229
|
});
|
|
17391
18230
|
}
|
|
17392
18231
|
parsed.push(r.data);
|
|
@@ -17493,10 +18332,15 @@ function registerRoutes(scope, ctx) {
|
|
|
17493
18332
|
}
|
|
17494
18333
|
const reg = built.registration;
|
|
17495
18334
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
18335
|
+
const incidentsPath = errorsPathFor(proj);
|
|
17496
18336
|
try {
|
|
17497
18337
|
const result = await ctx.runPoll(
|
|
17498
18338
|
reg.connector,
|
|
17499
|
-
{
|
|
18339
|
+
{
|
|
18340
|
+
projectDir: proj.scanPath ?? "",
|
|
18341
|
+
credentials: reg.credentials,
|
|
18342
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
18343
|
+
},
|
|
17500
18344
|
proj.graph,
|
|
17501
18345
|
reg.resolveTarget
|
|
17502
18346
|
);
|
|
@@ -17566,6 +18410,34 @@ function registerRoutes(scope, ctx) {
|
|
|
17566
18410
|
}
|
|
17567
18411
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
17568
18412
|
});
|
|
18413
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
18414
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18415
|
+
if (!proj) return;
|
|
18416
|
+
const { nodeId } = req.params;
|
|
18417
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18418
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18419
|
+
}
|
|
18420
|
+
const direction = req.query.direction;
|
|
18421
|
+
if (direction !== "up" && direction !== "down") {
|
|
18422
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18423
|
+
}
|
|
18424
|
+
const epath = errorsPathFor(proj);
|
|
18425
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18426
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18427
|
+
});
|
|
18428
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
18429
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18430
|
+
if (!proj) return;
|
|
18431
|
+
const { a, b } = req.query;
|
|
18432
|
+
if (!a || !b) {
|
|
18433
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18434
|
+
}
|
|
18435
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
18436
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18437
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18438
|
+
}
|
|
18439
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18440
|
+
});
|
|
17569
18441
|
scope.get("/search", async (req, reply) => {
|
|
17570
18442
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
17571
18443
|
if (!proj) return;
|
|
@@ -17695,7 +18567,7 @@ function registerRoutes(scope, ctx) {
|
|
|
17695
18567
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
17696
18568
|
let violations = await log.readAll();
|
|
17697
18569
|
if (req.query.severity) {
|
|
17698
|
-
const sev =
|
|
18570
|
+
const sev = import_types85.PolicySeveritySchema.safeParse(req.query.severity);
|
|
17699
18571
|
if (!sev.success) {
|
|
17700
18572
|
return reply.code(400).send({
|
|
17701
18573
|
error: "invalid severity",
|
|
@@ -17734,7 +18606,7 @@ function registerRoutes(scope, ctx) {
|
|
|
17734
18606
|
scope.post("/policies/check", async (req, reply) => {
|
|
17735
18607
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
17736
18608
|
if (!proj) return;
|
|
17737
|
-
const parsed =
|
|
18609
|
+
const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
17738
18610
|
if (!parsed.success) {
|
|
17739
18611
|
return reply.code(400).send({
|
|
17740
18612
|
error: "invalid /policies/check body",
|