@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
|
@@ -963,6 +963,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
963
963
|
import { extractedEdgeId } from "@neat.is/types";
|
|
964
964
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
965
965
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
966
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
966
967
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
967
968
|
"node_modules",
|
|
968
969
|
".git",
|
|
@@ -1002,6 +1003,7 @@ async function isPythonVenvDir(dir) {
|
|
|
1002
1003
|
function isConfigFile(name) {
|
|
1003
1004
|
const ext = path3.extname(name);
|
|
1004
1005
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1006
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
1005
1007
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
1006
1008
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
1007
1009
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -3339,6 +3341,62 @@ function columnIsObserved(col) {
|
|
|
3339
3341
|
return col.provenances.includes(Provenance4.OBSERVED);
|
|
3340
3342
|
}
|
|
3341
3343
|
|
|
3344
|
+
// src/latency-digest.ts
|
|
3345
|
+
var SUB = 16;
|
|
3346
|
+
var MIN_EXP = -10;
|
|
3347
|
+
var MAX_EXP = 22;
|
|
3348
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
3349
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
3350
|
+
function latencyBucketIndex(ms) {
|
|
3351
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
3352
|
+
const e = Math.floor(Math.log2(ms));
|
|
3353
|
+
if (e < MIN_EXP) return 0;
|
|
3354
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
3355
|
+
const base = 2 ** e;
|
|
3356
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
3357
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
3358
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
3359
|
+
}
|
|
3360
|
+
function bucketRepresentativeMs(index) {
|
|
3361
|
+
if (index <= 0) return 0;
|
|
3362
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
3363
|
+
const zeroBased = index - 1;
|
|
3364
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
3365
|
+
const s = zeroBased % SUB;
|
|
3366
|
+
const base = 2 ** e;
|
|
3367
|
+
const lower = base * (1 + s / SUB);
|
|
3368
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
3369
|
+
return (lower + upper) / 2;
|
|
3370
|
+
}
|
|
3371
|
+
function recordLatency(hist, ms) {
|
|
3372
|
+
const key = String(latencyBucketIndex(ms));
|
|
3373
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
3374
|
+
return hist;
|
|
3375
|
+
}
|
|
3376
|
+
function quantile(hist, q) {
|
|
3377
|
+
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]);
|
|
3378
|
+
let total = 0;
|
|
3379
|
+
for (const [, c] of entries) total += c;
|
|
3380
|
+
if (total === 0) return 0;
|
|
3381
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
3382
|
+
let cumulative = 0;
|
|
3383
|
+
for (const [idx, c] of entries) {
|
|
3384
|
+
cumulative += c;
|
|
3385
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
3386
|
+
}
|
|
3387
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
3388
|
+
}
|
|
3389
|
+
function round(ms) {
|
|
3390
|
+
return Math.round(ms * 100) / 100;
|
|
3391
|
+
}
|
|
3392
|
+
function latencyPercentiles(hist) {
|
|
3393
|
+
if (!hist) return void 0;
|
|
3394
|
+
let total = 0;
|
|
3395
|
+
for (const c of Object.values(hist)) total += c;
|
|
3396
|
+
if (total === 0) return void 0;
|
|
3397
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3342
3400
|
// src/ingest.ts
|
|
3343
3401
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
3344
3402
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -4034,7 +4092,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
4034
4092
|
graph.addNode(id, node);
|
|
4035
4093
|
return id;
|
|
4036
4094
|
}
|
|
4037
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
4095
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
4038
4096
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
4039
4097
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
4040
4098
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -4042,10 +4100,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
4042
4100
|
const existing = graph.getEdgeAttributes(id);
|
|
4043
4101
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
4044
4102
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
4103
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
4104
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
4045
4105
|
const newSignal = {
|
|
4046
4106
|
spanCount: newSpanCount,
|
|
4047
4107
|
errorCount: newErrorCount,
|
|
4048
|
-
lastObservedAgeMs: 0
|
|
4108
|
+
lastObservedAgeMs: 0,
|
|
4109
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
4110
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
4111
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
4049
4112
|
};
|
|
4050
4113
|
const updated = {
|
|
4051
4114
|
...existing,
|
|
@@ -4060,10 +4123,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
4060
4123
|
graph.replaceEdgeAttributes(id, updated);
|
|
4061
4124
|
return { edge: updated, created: false };
|
|
4062
4125
|
}
|
|
4126
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
4127
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
4063
4128
|
const signal = {
|
|
4064
4129
|
spanCount: 1,
|
|
4065
4130
|
errorCount: isError ? 1 : 0,
|
|
4066
|
-
lastObservedAgeMs: 0
|
|
4131
|
+
lastObservedAgeMs: 0,
|
|
4132
|
+
...latencyHist ? { latencyHist } : {},
|
|
4133
|
+
...latencyMs ? { latencyMs } : {}
|
|
4067
4134
|
};
|
|
4068
4135
|
const edge = {
|
|
4069
4136
|
id,
|
|
@@ -4127,6 +4194,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
4127
4194
|
await fs7.mkdir(path8.dirname(ctx.errorsPath), { recursive: true });
|
|
4128
4195
|
await fs7.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4129
4196
|
}
|
|
4197
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
4198
|
+
const ev = {
|
|
4199
|
+
id: input.id,
|
|
4200
|
+
timestamp: input.timestamp,
|
|
4201
|
+
service: input.service,
|
|
4202
|
+
traceId: input.id,
|
|
4203
|
+
spanId: input.id,
|
|
4204
|
+
errorType: input.errorType,
|
|
4205
|
+
errorMessage: input.errorMessage,
|
|
4206
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
4207
|
+
affectedNode: input.affectedNode
|
|
4208
|
+
};
|
|
4209
|
+
await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
|
|
4210
|
+
await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4211
|
+
}
|
|
4130
4212
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
4131
4213
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : serviceId(span.service, span.env);
|
|
4132
4214
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -4273,6 +4355,7 @@ async function handleSpan(ctx, span) {
|
|
|
4273
4355
|
}
|
|
4274
4356
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
4275
4357
|
const isError = span.statusCode === 2;
|
|
4358
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
4276
4359
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
4277
4360
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
4278
4361
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -4311,7 +4394,8 @@ async function handleSpan(ctx, span) {
|
|
|
4311
4394
|
targetId,
|
|
4312
4395
|
ts,
|
|
4313
4396
|
isError,
|
|
4314
|
-
callSiteEvidence
|
|
4397
|
+
callSiteEvidence,
|
|
4398
|
+
durationMs
|
|
4315
4399
|
);
|
|
4316
4400
|
if (result) affectedNode = targetId;
|
|
4317
4401
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -4323,7 +4407,8 @@ async function handleSpan(ctx, span) {
|
|
|
4323
4407
|
collectionId,
|
|
4324
4408
|
ts,
|
|
4325
4409
|
isError,
|
|
4326
|
-
callSiteEvidence
|
|
4410
|
+
callSiteEvidence,
|
|
4411
|
+
durationMs
|
|
4327
4412
|
);
|
|
4328
4413
|
}
|
|
4329
4414
|
if (span.dbTable) {
|
|
@@ -4335,7 +4420,8 @@ async function handleSpan(ctx, span) {
|
|
|
4335
4420
|
tableId,
|
|
4336
4421
|
ts,
|
|
4337
4422
|
isError,
|
|
4338
|
-
callSiteEvidence
|
|
4423
|
+
callSiteEvidence,
|
|
4424
|
+
durationMs
|
|
4339
4425
|
);
|
|
4340
4426
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
4341
4427
|
}
|
|
@@ -4354,7 +4440,8 @@ async function handleSpan(ctx, span) {
|
|
|
4354
4440
|
targetId,
|
|
4355
4441
|
ts,
|
|
4356
4442
|
isError,
|
|
4357
|
-
callSiteEvidence
|
|
4443
|
+
callSiteEvidence,
|
|
4444
|
+
durationMs
|
|
4358
4445
|
);
|
|
4359
4446
|
if (result) affectedNode = targetId;
|
|
4360
4447
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -4371,7 +4458,8 @@ async function handleSpan(ctx, span) {
|
|
|
4371
4458
|
targetId,
|
|
4372
4459
|
ts,
|
|
4373
4460
|
isError,
|
|
4374
|
-
callSiteEvidence
|
|
4461
|
+
callSiteEvidence,
|
|
4462
|
+
durationMs
|
|
4375
4463
|
);
|
|
4376
4464
|
if (result) affectedNode = targetId;
|
|
4377
4465
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -4383,7 +4471,8 @@ async function handleSpan(ctx, span) {
|
|
|
4383
4471
|
targetId,
|
|
4384
4472
|
ts,
|
|
4385
4473
|
isError,
|
|
4386
|
-
callSiteEvidence
|
|
4474
|
+
callSiteEvidence,
|
|
4475
|
+
durationMs
|
|
4387
4476
|
);
|
|
4388
4477
|
if (result) affectedNode = targetId;
|
|
4389
4478
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -4399,7 +4488,8 @@ async function handleSpan(ctx, span) {
|
|
|
4399
4488
|
targetId,
|
|
4400
4489
|
ts,
|
|
4401
4490
|
isError,
|
|
4402
|
-
callSiteEvidence
|
|
4491
|
+
callSiteEvidence,
|
|
4492
|
+
durationMs
|
|
4403
4493
|
);
|
|
4404
4494
|
if (result) affectedNode = targetId;
|
|
4405
4495
|
} else {
|
|
@@ -4415,7 +4505,8 @@ async function handleSpan(ctx, span) {
|
|
|
4415
4505
|
targetId,
|
|
4416
4506
|
ts,
|
|
4417
4507
|
isError,
|
|
4418
|
-
callSiteEvidence
|
|
4508
|
+
callSiteEvidence,
|
|
4509
|
+
durationMs
|
|
4419
4510
|
);
|
|
4420
4511
|
affectedNode = targetId;
|
|
4421
4512
|
resolvedViaAddress = true;
|
|
@@ -4428,7 +4519,8 @@ async function handleSpan(ctx, span) {
|
|
|
4428
4519
|
frontierNodeId,
|
|
4429
4520
|
ts,
|
|
4430
4521
|
isError,
|
|
4431
|
-
callSiteEvidence
|
|
4522
|
+
callSiteEvidence,
|
|
4523
|
+
durationMs
|
|
4432
4524
|
);
|
|
4433
4525
|
affectedNode = frontierNodeId;
|
|
4434
4526
|
resolvedViaAddress = true;
|
|
@@ -4454,7 +4546,8 @@ async function handleSpan(ctx, span) {
|
|
|
4454
4546
|
sourceId,
|
|
4455
4547
|
ts,
|
|
4456
4548
|
isError,
|
|
4457
|
-
fallbackEvidence
|
|
4549
|
+
fallbackEvidence,
|
|
4550
|
+
durationMs
|
|
4458
4551
|
);
|
|
4459
4552
|
}
|
|
4460
4553
|
}
|
|
@@ -4468,7 +4561,7 @@ async function handleSpan(ctx, span) {
|
|
|
4468
4561
|
);
|
|
4469
4562
|
if (routeNodeId) {
|
|
4470
4563
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
4471
|
-
upsertObservedEdge(ctx.graph, EdgeType5.CONTAINS, serviceId(routeSvc), routeNodeId, ts, isError);
|
|
4564
|
+
upsertObservedEdge(ctx.graph, EdgeType5.CONTAINS, serviceId(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
4472
4565
|
}
|
|
4473
4566
|
}
|
|
4474
4567
|
if (span.statusCode === 2) {
|
|
@@ -4757,10 +4850,12 @@ function mergeSnapshot(graph, snapshot) {
|
|
|
4757
4850
|
import {
|
|
4758
4851
|
BlastRadiusResultSchema,
|
|
4759
4852
|
EdgeType as EdgeType6,
|
|
4853
|
+
ExpandResultSchema,
|
|
4760
4854
|
NodeType as NodeType5,
|
|
4761
4855
|
ObservedDependenciesResultSchema,
|
|
4762
4856
|
PROV_RANK,
|
|
4763
4857
|
Provenance as Provenance6,
|
|
4858
|
+
RelateResultSchema,
|
|
4764
4859
|
RootCauseResultSchema,
|
|
4765
4860
|
TransitiveDependenciesResultSchema
|
|
4766
4861
|
} from "@neat.is/types";
|
|
@@ -4977,7 +5072,7 @@ var rootCauseShapes = {
|
|
|
4977
5072
|
[NodeType5.FileNode]: fileRootCauseShape,
|
|
4978
5073
|
[NodeType5.SymbolNode]: symbolRootCauseShape
|
|
4979
5074
|
};
|
|
4980
|
-
function
|
|
5075
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
4981
5076
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
4982
5077
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
4983
5078
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -5234,7 +5329,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5234
5329
|
dependencies: [],
|
|
5235
5330
|
observed: false,
|
|
5236
5331
|
inboundObservedCount: 0,
|
|
5237
|
-
hasExtractedOutbound: false
|
|
5332
|
+
hasExtractedOutbound: false,
|
|
5333
|
+
inboundVolume: 0,
|
|
5334
|
+
window: "lifetime"
|
|
5238
5335
|
});
|
|
5239
5336
|
}
|
|
5240
5337
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -5265,11 +5362,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5265
5362
|
}
|
|
5266
5363
|
}
|
|
5267
5364
|
let inboundObservedCount = 0;
|
|
5365
|
+
let inboundVolume = 0;
|
|
5366
|
+
let inboundLastObserved;
|
|
5268
5367
|
for (const tgt of scope) {
|
|
5269
5368
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
5270
5369
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5271
5370
|
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5272
|
-
if (e.provenance === Provenance6.OBSERVED)
|
|
5371
|
+
if (e.provenance === Provenance6.OBSERVED) {
|
|
5372
|
+
inboundObservedCount += 1;
|
|
5373
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
5374
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
5375
|
+
inboundLastObserved = e.lastObserved;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5273
5378
|
}
|
|
5274
5379
|
}
|
|
5275
5380
|
dependencies.sort(
|
|
@@ -5280,7 +5385,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
5280
5385
|
dependencies,
|
|
5281
5386
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
5282
5387
|
inboundObservedCount,
|
|
5283
|
-
hasExtractedOutbound
|
|
5388
|
+
hasExtractedOutbound,
|
|
5389
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
5390
|
+
inboundVolume,
|
|
5391
|
+
window: "lifetime",
|
|
5392
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
5393
|
+
});
|
|
5394
|
+
}
|
|
5395
|
+
var SATURATION_P95_MS = 1e3;
|
|
5396
|
+
function nodeScope(graph, nodeId) {
|
|
5397
|
+
const scope = [nodeId];
|
|
5398
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
5399
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
5400
|
+
if (attrs.type === NodeType5.ServiceNode) {
|
|
5401
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
5402
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5403
|
+
if (e.type !== EdgeType6.CONTAINS) continue;
|
|
5404
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
5405
|
+
if (owned.type === NodeType5.FileNode) scope.push(e.target);
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
return scope;
|
|
5409
|
+
}
|
|
5410
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
5411
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
5412
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
5413
|
+
}
|
|
5414
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
5415
|
+
const scope = nodeScope(graph, nodeId);
|
|
5416
|
+
let errorsFromCallers = 0;
|
|
5417
|
+
let inboundVolume = 0;
|
|
5418
|
+
let outboundVolume = 0;
|
|
5419
|
+
let outboundErrors = 0;
|
|
5420
|
+
let latestInboundMs;
|
|
5421
|
+
let latencyP95Ms;
|
|
5422
|
+
let stale = false;
|
|
5423
|
+
for (const n of scope) {
|
|
5424
|
+
if (!graph.hasNode(n)) continue;
|
|
5425
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
5426
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5427
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5428
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
5429
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
5430
|
+
if (e.provenance === Provenance6.STALE) stale = true;
|
|
5431
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
5432
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
5433
|
+
if (e.lastObserved) {
|
|
5434
|
+
const t = Date.parse(e.lastObserved);
|
|
5435
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
5439
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5440
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5441
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
5442
|
+
if (e.type === EdgeType6.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
5443
|
+
if (e.provenance === Provenance6.STALE) stale = true;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
5447
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
5448
|
+
return {
|
|
5449
|
+
errorsEmittedHere,
|
|
5450
|
+
errorsFromCallers,
|
|
5451
|
+
callCount: inboundVolume,
|
|
5452
|
+
outboundVolume,
|
|
5453
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
5454
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
5455
|
+
stale
|
|
5456
|
+
};
|
|
5457
|
+
}
|
|
5458
|
+
function isSaturated(ctx) {
|
|
5459
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
5460
|
+
}
|
|
5461
|
+
function classifyNode(ctx) {
|
|
5462
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
5463
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
5464
|
+
return "symptom-only";
|
|
5465
|
+
}
|
|
5466
|
+
return "primary-failure";
|
|
5467
|
+
}
|
|
5468
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
5469
|
+
return "unrelated";
|
|
5470
|
+
}
|
|
5471
|
+
function isVictimSeed(ctx) {
|
|
5472
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
5473
|
+
}
|
|
5474
|
+
function grainOf(graph, nodeId) {
|
|
5475
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
5476
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
5477
|
+
if (t === NodeType5.ServiceNode) return "service";
|
|
5478
|
+
if (t === NodeType5.FileNode) return "file";
|
|
5479
|
+
if (t === NodeType5.SymbolNode) return "symbol";
|
|
5480
|
+
return t;
|
|
5481
|
+
}
|
|
5482
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
5483
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
5484
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
5485
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
5486
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
5487
|
+
while (queue.length > 0) {
|
|
5488
|
+
const frame = queue.shift();
|
|
5489
|
+
if (frame.depth >= maxDepth) continue;
|
|
5490
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
5491
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
5492
|
+
for (const [nid, edge] of neighbours) {
|
|
5493
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
5494
|
+
if (enqueued.has(nid)) continue;
|
|
5495
|
+
enqueued.add(nid);
|
|
5496
|
+
queue.push({
|
|
5497
|
+
nodeId: nid,
|
|
5498
|
+
depth: frame.depth + 1,
|
|
5499
|
+
nodes: [...frame.nodes, nid],
|
|
5500
|
+
edges: [...frame.edges, edge]
|
|
5501
|
+
});
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
return null;
|
|
5505
|
+
}
|
|
5506
|
+
var EMPTY_CONTEXT = {
|
|
5507
|
+
errorsEmittedHere: 0,
|
|
5508
|
+
errorsFromCallers: 0,
|
|
5509
|
+
callCount: 0,
|
|
5510
|
+
outboundVolume: 0,
|
|
5511
|
+
stale: false
|
|
5512
|
+
};
|
|
5513
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
5514
|
+
if (!graph.hasNode(nodeId)) {
|
|
5515
|
+
return ExpandResultSchema.parse({
|
|
5516
|
+
origin: nodeId,
|
|
5517
|
+
direction,
|
|
5518
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
5519
|
+
neighbours: []
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
5523
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
5524
|
+
const neighbours = [];
|
|
5525
|
+
for (const [nid, edge] of best) {
|
|
5526
|
+
if (edge.type === EdgeType6.CONTAINS) continue;
|
|
5527
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
5528
|
+
neighbours.push({
|
|
5529
|
+
node: nid,
|
|
5530
|
+
edgeType: edge.type,
|
|
5531
|
+
provenance: edge.provenance,
|
|
5532
|
+
classification: classifyNode(nctx),
|
|
5533
|
+
context: nctx
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
5537
|
+
return ExpandResultSchema.parse({
|
|
5538
|
+
origin: nodeId,
|
|
5539
|
+
direction,
|
|
5540
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
5541
|
+
neighbours
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
5545
|
+
const buildPath = (fp) => ({
|
|
5546
|
+
nodes: fp.nodes,
|
|
5547
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
5548
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
5549
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
5550
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
5551
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
5552
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
5553
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
5554
|
+
)
|
|
5555
|
+
});
|
|
5556
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
5557
|
+
return RelateResultSchema.parse({
|
|
5558
|
+
a,
|
|
5559
|
+
b,
|
|
5560
|
+
related: false,
|
|
5561
|
+
direction: null,
|
|
5562
|
+
paths: [],
|
|
5563
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
5564
|
+
});
|
|
5565
|
+
}
|
|
5566
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
5567
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
5568
|
+
if (!down && !up) {
|
|
5569
|
+
return RelateResultSchema.parse({
|
|
5570
|
+
a,
|
|
5571
|
+
b,
|
|
5572
|
+
related: false,
|
|
5573
|
+
direction: null,
|
|
5574
|
+
paths: [],
|
|
5575
|
+
note: `no path within ${maxDepth} hops`
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
const paths = [];
|
|
5579
|
+
const direction = down ? "a->b" : "b->a";
|
|
5580
|
+
if (down) paths.push(buildPath(down));
|
|
5581
|
+
if (up) paths.push(buildPath(up));
|
|
5582
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
5583
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
5584
|
+
return RelateResultSchema.parse({
|
|
5585
|
+
a,
|
|
5586
|
+
b,
|
|
5587
|
+
related: true,
|
|
5588
|
+
direction,
|
|
5589
|
+
paths,
|
|
5590
|
+
...grainGap ? { grainGap: true } : {}
|
|
5591
|
+
});
|
|
5592
|
+
}
|
|
5593
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
5594
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
5595
|
+
let best = null;
|
|
5596
|
+
for (const nid of upstream) {
|
|
5597
|
+
if (nid === alertNodeId) continue;
|
|
5598
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
5599
|
+
if (ctx.outboundVolume === 0) continue;
|
|
5600
|
+
const isSource = ctx.callCount === 0;
|
|
5601
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
5602
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
5603
|
+
}
|
|
5604
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
5605
|
+
}
|
|
5606
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
5607
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
5608
|
+
if (!legacy) return null;
|
|
5609
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
5610
|
+
if (!navigation) return legacy;
|
|
5611
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
5612
|
+
}
|
|
5613
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
5614
|
+
const seedNode = legacy.rootCauseNode;
|
|
5615
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
5616
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
5617
|
+
const candidates = [];
|
|
5618
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
5619
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
5620
|
+
if (origin) {
|
|
5621
|
+
const path64 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
5622
|
+
const originConfidence = confidenceFromMix(path64?.edges ?? [], now);
|
|
5623
|
+
candidates.push({
|
|
5624
|
+
node: origin.node,
|
|
5625
|
+
classification: "primary-failure",
|
|
5626
|
+
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.`,
|
|
5627
|
+
context: origin.ctx,
|
|
5628
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
5629
|
+
provenance: Provenance6.OBSERVED
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
5633
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
5634
|
+
candidates.push({
|
|
5635
|
+
node: seedNode,
|
|
5636
|
+
classification: "symptom-only",
|
|
5637
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
5638
|
+
context: seedCtx,
|
|
5639
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
5640
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
5641
|
+
});
|
|
5642
|
+
} else {
|
|
5643
|
+
candidates.push({
|
|
5644
|
+
node: seedNode,
|
|
5645
|
+
classification: "primary-failure",
|
|
5646
|
+
reason: legacy.rootCauseReason,
|
|
5647
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
5648
|
+
confidence: legacy.confidence,
|
|
5649
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
5652
|
+
const top = candidates[0];
|
|
5653
|
+
let traversalPath = legacy.traversalPath;
|
|
5654
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
5655
|
+
if (top.node !== seedNode) {
|
|
5656
|
+
const path64 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
5657
|
+
if (path64) {
|
|
5658
|
+
traversalPath = path64.nodes;
|
|
5659
|
+
edgeProvenances = path64.edges.map((e) => e.provenance);
|
|
5660
|
+
} else {
|
|
5661
|
+
traversalPath = [errorNodeId, top.node];
|
|
5662
|
+
edgeProvenances = [top.provenance ?? Provenance6.OBSERVED];
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
return RootCauseResultSchema.parse({
|
|
5666
|
+
rootCauseNode: top.node,
|
|
5667
|
+
rootCauseReason: top.reason,
|
|
5668
|
+
traversalPath,
|
|
5669
|
+
edgeProvenances,
|
|
5670
|
+
confidence: top.confidence,
|
|
5671
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
5672
|
+
candidates
|
|
5284
5673
|
});
|
|
5285
5674
|
}
|
|
5286
5675
|
|
|
@@ -10488,8 +10877,41 @@ import path45 from "path";
|
|
|
10488
10877
|
import Parser14 from "tree-sitter";
|
|
10489
10878
|
import Go3 from "tree-sitter-go";
|
|
10490
10879
|
import { infraId as infraId15 } from "@neat.is/types";
|
|
10491
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
10492
10880
|
var PARSE_CHUNK10 = 16384;
|
|
10881
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
10882
|
+
"Query",
|
|
10883
|
+
"QueryContext",
|
|
10884
|
+
"QueryRow",
|
|
10885
|
+
"QueryRowContext",
|
|
10886
|
+
"Exec",
|
|
10887
|
+
"ExecContext",
|
|
10888
|
+
"Prepare",
|
|
10889
|
+
"PrepareContext"
|
|
10890
|
+
]);
|
|
10891
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
10892
|
+
"Get",
|
|
10893
|
+
"Select",
|
|
10894
|
+
"Queryx",
|
|
10895
|
+
"QueryRowx",
|
|
10896
|
+
"NamedExec",
|
|
10897
|
+
"NamedQuery",
|
|
10898
|
+
"MustExec",
|
|
10899
|
+
"Preparex",
|
|
10900
|
+
"GetContext",
|
|
10901
|
+
"SelectContext"
|
|
10902
|
+
]);
|
|
10903
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
10904
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
10905
|
+
function makeGoParser3() {
|
|
10906
|
+
const p = new Parser14();
|
|
10907
|
+
p.setLanguage(Go3);
|
|
10908
|
+
return p;
|
|
10909
|
+
}
|
|
10910
|
+
function parseSource10(parser, source) {
|
|
10911
|
+
return parser.parse(
|
|
10912
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
10913
|
+
);
|
|
10914
|
+
}
|
|
10493
10915
|
function walk7(node, visit) {
|
|
10494
10916
|
visit(node);
|
|
10495
10917
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -10497,25 +10919,54 @@ function walk7(node, visit) {
|
|
|
10497
10919
|
if (child) walk7(child, visit);
|
|
10498
10920
|
}
|
|
10499
10921
|
}
|
|
10922
|
+
function goStringLiteralValue(node) {
|
|
10923
|
+
if (!node) return null;
|
|
10924
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
10925
|
+
const t = node.text;
|
|
10926
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
10927
|
+
}
|
|
10928
|
+
return null;
|
|
10929
|
+
}
|
|
10930
|
+
function goImportsAny(root, names) {
|
|
10931
|
+
let found = false;
|
|
10932
|
+
walk7(root, (node) => {
|
|
10933
|
+
if (found || node.type !== "import_spec") return;
|
|
10934
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
10935
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
10936
|
+
if (value !== null && names.has(value)) found = true;
|
|
10937
|
+
}
|
|
10938
|
+
});
|
|
10939
|
+
return found;
|
|
10940
|
+
}
|
|
10941
|
+
function firstStringLiteralArg(argsNode) {
|
|
10942
|
+
if (!argsNode) return null;
|
|
10943
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
10944
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
10945
|
+
if (value !== null) return value;
|
|
10946
|
+
}
|
|
10947
|
+
return null;
|
|
10948
|
+
}
|
|
10500
10949
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
10501
10950
|
if (path45.extname(file.path) !== ".go") return [];
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
const
|
|
10505
|
-
|
|
10506
|
-
);
|
|
10951
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
10952
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
10953
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
10954
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
10955
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
10507
10956
|
const out = [];
|
|
10508
10957
|
walk7(tree.rootNode, (node) => {
|
|
10509
10958
|
if (node.type !== "call_expression") return;
|
|
10510
10959
|
const fn = node.childForFieldName("function");
|
|
10511
10960
|
if (fn?.type !== "selector_expression") return;
|
|
10512
10961
|
const method = fn.childForFieldName("field")?.text;
|
|
10513
|
-
if (!method
|
|
10514
|
-
const
|
|
10515
|
-
if (!
|
|
10516
|
-
const sql =
|
|
10962
|
+
if (!method) return;
|
|
10963
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
10964
|
+
if (!recognized) return;
|
|
10965
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
10966
|
+
if (sql === null) return;
|
|
10517
10967
|
const table = tableFromSqlStatement(sql);
|
|
10518
10968
|
if (!table) return;
|
|
10969
|
+
const columns = columnsFromSqlStatement(sql);
|
|
10519
10970
|
const line = node.startPosition.row + 1;
|
|
10520
10971
|
out.push({
|
|
10521
10972
|
infraId: infraId15("sql-table", table),
|
|
@@ -10523,7 +10974,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
10523
10974
|
kind: "sql-table",
|
|
10524
10975
|
edgeType: "CALLS",
|
|
10525
10976
|
confidenceKind: "verified-call-site",
|
|
10526
|
-
|
|
10977
|
+
...columns.length > 0 ? { columns } : {},
|
|
10978
|
+
evidence: {
|
|
10979
|
+
file: toPosix(path45.relative(serviceDir, file.path)),
|
|
10980
|
+
line,
|
|
10981
|
+
snippet: snippet(file.content, line)
|
|
10982
|
+
}
|
|
10527
10983
|
});
|
|
10528
10984
|
});
|
|
10529
10985
|
return out;
|
|
@@ -10536,12 +10992,12 @@ import Go4 from "tree-sitter-go";
|
|
|
10536
10992
|
import { infraId as infraId16 } from "@neat.is/types";
|
|
10537
10993
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
10538
10994
|
var PARSE_CHUNK11 = 16384;
|
|
10539
|
-
function
|
|
10995
|
+
function makeGoParser4() {
|
|
10540
10996
|
const p = new Parser15();
|
|
10541
10997
|
p.setLanguage(Go4);
|
|
10542
10998
|
return p;
|
|
10543
10999
|
}
|
|
10544
|
-
function
|
|
11000
|
+
function parseSource11(parser, source) {
|
|
10545
11001
|
return parser.parse(
|
|
10546
11002
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
10547
11003
|
);
|
|
@@ -10965,7 +11421,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
10965
11421
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
10966
11422
|
if (path46.extname(file.path) !== ".go") return [];
|
|
10967
11423
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
10968
|
-
const tree =
|
|
11424
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
10969
11425
|
const { structs, models, tableFor } = analyze(tree);
|
|
10970
11426
|
const out = [];
|
|
10971
11427
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -10996,7 +11452,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
10996
11452
|
function gormForeignKeys(file, serviceDir) {
|
|
10997
11453
|
if (path46.extname(file.path) !== ".go") return [];
|
|
10998
11454
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
10999
|
-
const tree =
|
|
11455
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11000
11456
|
const { structs, models, tableFor } = analyze(tree);
|
|
11001
11457
|
const out = [];
|
|
11002
11458
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14420,6 +14876,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
14420
14876
|
unresolved++;
|
|
14421
14877
|
continue;
|
|
14422
14878
|
}
|
|
14879
|
+
if (signal.incident) {
|
|
14880
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
14881
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
14882
|
+
unresolved++;
|
|
14883
|
+
continue;
|
|
14884
|
+
}
|
|
14885
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
14886
|
+
id: signal.incident.id,
|
|
14887
|
+
timestamp: signal.incident.timestamp,
|
|
14888
|
+
service: signal.incident.service,
|
|
14889
|
+
errorType: signal.incident.errorType,
|
|
14890
|
+
errorMessage: signal.incident.errorMessage,
|
|
14891
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
14892
|
+
affectedNode: resolved.targetNodeId
|
|
14893
|
+
});
|
|
14894
|
+
continue;
|
|
14895
|
+
}
|
|
14423
14896
|
if (resolved.ensureInfraNode) {
|
|
14424
14897
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
14425
14898
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -16587,6 +17060,329 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
16587
17060
|
};
|
|
16588
17061
|
}
|
|
16589
17062
|
|
|
17063
|
+
// src/connectors/eas/types.ts
|
|
17064
|
+
function readEasCredentials(raw) {
|
|
17065
|
+
const token = raw["token"];
|
|
17066
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
17067
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
17068
|
+
}
|
|
17069
|
+
return { token };
|
|
17070
|
+
}
|
|
17071
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
17072
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
17073
|
+
"SPIN_UP_BUILDER",
|
|
17074
|
+
"PREPARE_CREDENTIALS",
|
|
17075
|
+
"RESTORE_CACHE",
|
|
17076
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
17077
|
+
]);
|
|
17078
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
17079
|
+
function isTransientFailure(err) {
|
|
17080
|
+
if (!err) return false;
|
|
17081
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
17082
|
+
if (phase) {
|
|
17083
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
17084
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
17085
|
+
}
|
|
17086
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
17087
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
17088
|
+
return false;
|
|
17089
|
+
}
|
|
17090
|
+
var FIELD_SEP3 = "\0";
|
|
17091
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
17092
|
+
function packEasTargetName(identity) {
|
|
17093
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
17094
|
+
}
|
|
17095
|
+
function parseEasTargetName(targetName) {
|
|
17096
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
17097
|
+
if (sep === -1) return null;
|
|
17098
|
+
const serviceName = targetName.slice(0, sep);
|
|
17099
|
+
const phase = targetName.slice(sep + 1);
|
|
17100
|
+
if (!serviceName) return null;
|
|
17101
|
+
return { serviceName, phase };
|
|
17102
|
+
}
|
|
17103
|
+
|
|
17104
|
+
// src/connectors/eas/client.ts
|
|
17105
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
17106
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
17107
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
17108
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
17109
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
17110
|
+
var BUILDS_QUERY = `
|
|
17111
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
17112
|
+
app {
|
|
17113
|
+
byId(appId: $appId) {
|
|
17114
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
17115
|
+
id
|
|
17116
|
+
status
|
|
17117
|
+
platform
|
|
17118
|
+
buildProfile
|
|
17119
|
+
gitCommitHash
|
|
17120
|
+
gitCommitMessage
|
|
17121
|
+
gitRef
|
|
17122
|
+
isGitWorkingTreeDirty
|
|
17123
|
+
createdAt
|
|
17124
|
+
completedAt
|
|
17125
|
+
error {
|
|
17126
|
+
buildPhase
|
|
17127
|
+
errorCode
|
|
17128
|
+
message
|
|
17129
|
+
docsUrl
|
|
17130
|
+
}
|
|
17131
|
+
logFileUrls
|
|
17132
|
+
}
|
|
17133
|
+
}
|
|
17134
|
+
}
|
|
17135
|
+
}
|
|
17136
|
+
`;
|
|
17137
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
17138
|
+
const res = await junctionFetch(
|
|
17139
|
+
apiUrl,
|
|
17140
|
+
{
|
|
17141
|
+
method: "POST",
|
|
17142
|
+
headers: {
|
|
17143
|
+
"Content-Type": "application/json",
|
|
17144
|
+
...bearerAuthHeader(token)
|
|
17145
|
+
},
|
|
17146
|
+
body: JSON.stringify({ query, variables })
|
|
17147
|
+
},
|
|
17148
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
17149
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
17150
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
17151
|
+
);
|
|
17152
|
+
if (!res.ok) {
|
|
17153
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
17154
|
+
}
|
|
17155
|
+
const body = await res.json();
|
|
17156
|
+
if (body.errors && body.errors.length > 0) {
|
|
17157
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
17158
|
+
}
|
|
17159
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
17160
|
+
return body.data;
|
|
17161
|
+
}
|
|
17162
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
17163
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
17164
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
17165
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
17166
|
+
const out = [];
|
|
17167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17168
|
+
for (let page = 0; page < maxPages; page++) {
|
|
17169
|
+
const data = await easGraphQL(
|
|
17170
|
+
apiUrl,
|
|
17171
|
+
token,
|
|
17172
|
+
BUILDS_QUERY,
|
|
17173
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
17174
|
+
config.appId,
|
|
17175
|
+
fetchImpl
|
|
17176
|
+
);
|
|
17177
|
+
const builds = data.app?.byId?.builds;
|
|
17178
|
+
if (!Array.isArray(builds)) break;
|
|
17179
|
+
let added = 0;
|
|
17180
|
+
for (const b of builds) {
|
|
17181
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
17182
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
17183
|
+
if (seen.has(b.id)) continue;
|
|
17184
|
+
seen.add(b.id);
|
|
17185
|
+
out.push(b);
|
|
17186
|
+
added++;
|
|
17187
|
+
}
|
|
17188
|
+
if (builds.length < pageSize) break;
|
|
17189
|
+
if (added === 0) break;
|
|
17190
|
+
}
|
|
17191
|
+
return out;
|
|
17192
|
+
}
|
|
17193
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
17194
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
17195
|
+
const doFetch = fetchImpl ?? fetch;
|
|
17196
|
+
const chunks = [];
|
|
17197
|
+
for (const url of logFileUrls) {
|
|
17198
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
17199
|
+
try {
|
|
17200
|
+
const res = await doFetch(url);
|
|
17201
|
+
if (!res.ok) continue;
|
|
17202
|
+
chunks.push(await res.text());
|
|
17203
|
+
} catch {
|
|
17204
|
+
}
|
|
17205
|
+
}
|
|
17206
|
+
const joined = chunks.join("\n");
|
|
17207
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
17208
|
+
}
|
|
17209
|
+
|
|
17210
|
+
// src/connectors/eas/map.ts
|
|
17211
|
+
function buildEventTime(build) {
|
|
17212
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
17213
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
17214
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
17215
|
+
}
|
|
17216
|
+
function incidentMessage2(build) {
|
|
17217
|
+
const err = build.error ?? {};
|
|
17218
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
17219
|
+
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";
|
|
17220
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
17221
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
17222
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
17223
|
+
}
|
|
17224
|
+
return msg;
|
|
17225
|
+
}
|
|
17226
|
+
function incidentAttributes(build) {
|
|
17227
|
+
const attrs = {};
|
|
17228
|
+
const err = build.error ?? {};
|
|
17229
|
+
const put = (k, v) => {
|
|
17230
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
17231
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
17232
|
+
};
|
|
17233
|
+
put("eas.buildId", build.id);
|
|
17234
|
+
put("eas.platform", build.platform ?? void 0);
|
|
17235
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
17236
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
17237
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
17238
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
17239
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
17240
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
17241
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
17242
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
17243
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
17244
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
17245
|
+
}
|
|
17246
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
17247
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
17248
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
17249
|
+
attrs["eas.logs"] = build.logsText;
|
|
17250
|
+
}
|
|
17251
|
+
return attrs;
|
|
17252
|
+
}
|
|
17253
|
+
function mapBuildToSignal(build, serviceName) {
|
|
17254
|
+
if (!build || typeof build !== "object") return null;
|
|
17255
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
17256
|
+
if (!build.error) return null;
|
|
17257
|
+
if (isTransientFailure(build.error)) return null;
|
|
17258
|
+
const timestamp = buildEventTime(build);
|
|
17259
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
17260
|
+
return {
|
|
17261
|
+
targetKind: EAS_TARGET_KIND,
|
|
17262
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
17263
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
17264
|
+
callCount: 0,
|
|
17265
|
+
errorCount: 0,
|
|
17266
|
+
lastObservedIso: timestamp,
|
|
17267
|
+
incident: {
|
|
17268
|
+
id: `eas:build:${build.id}`,
|
|
17269
|
+
timestamp,
|
|
17270
|
+
service: serviceName,
|
|
17271
|
+
errorType: "eas-build-failure",
|
|
17272
|
+
errorMessage: incidentMessage2(build),
|
|
17273
|
+
attributes: incidentAttributes(build)
|
|
17274
|
+
}
|
|
17275
|
+
};
|
|
17276
|
+
}
|
|
17277
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
17278
|
+
const out = [];
|
|
17279
|
+
for (const build of builds) {
|
|
17280
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
17281
|
+
if (signal) out.push(signal);
|
|
17282
|
+
}
|
|
17283
|
+
return out;
|
|
17284
|
+
}
|
|
17285
|
+
|
|
17286
|
+
// src/connectors/eas/resolve.ts
|
|
17287
|
+
import { EdgeType as EdgeType34, NodeType as NodeType32, parseFileId as parseFileId3 } from "@neat.is/types";
|
|
17288
|
+
var NO_ENV2 = "unknown";
|
|
17289
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
17290
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
17291
|
+
"READ_APP_CONFIG",
|
|
17292
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
17293
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
17294
|
+
]);
|
|
17295
|
+
function configBasenamesForPhase(phase) {
|
|
17296
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
17297
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
17298
|
+
return [];
|
|
17299
|
+
}
|
|
17300
|
+
function configNodeService(graph, configNodeId) {
|
|
17301
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
17302
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
17303
|
+
if (edge.type !== EdgeType34.CONFIGURED_BY) continue;
|
|
17304
|
+
const parsed = parseFileId3(edge.source);
|
|
17305
|
+
if (parsed) return parsed.service;
|
|
17306
|
+
}
|
|
17307
|
+
return null;
|
|
17308
|
+
}
|
|
17309
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
17310
|
+
let scoped = null;
|
|
17311
|
+
let anyMatch = null;
|
|
17312
|
+
graph.forEachNode((id, attrs) => {
|
|
17313
|
+
if (scoped) return;
|
|
17314
|
+
const node = attrs;
|
|
17315
|
+
if (node.type !== NodeType32.ConfigNode) return;
|
|
17316
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
17317
|
+
if (anyMatch === null) anyMatch = id;
|
|
17318
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
17319
|
+
});
|
|
17320
|
+
return scoped ?? anyMatch;
|
|
17321
|
+
}
|
|
17322
|
+
function createEasResolveTarget(graph) {
|
|
17323
|
+
return (signal) => {
|
|
17324
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
17325
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
17326
|
+
if (!identity) return null;
|
|
17327
|
+
const { serviceName, phase } = identity;
|
|
17328
|
+
const basenames = configBasenamesForPhase(phase);
|
|
17329
|
+
if (basenames.length > 0) {
|
|
17330
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
17331
|
+
if (configNodeId) {
|
|
17332
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: EdgeType34.CALLS };
|
|
17333
|
+
}
|
|
17334
|
+
}
|
|
17335
|
+
return {
|
|
17336
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
17337
|
+
serviceName,
|
|
17338
|
+
edgeType: EdgeType34.CALLS
|
|
17339
|
+
};
|
|
17340
|
+
};
|
|
17341
|
+
}
|
|
17342
|
+
|
|
17343
|
+
// src/connectors/eas/index.ts
|
|
17344
|
+
function isBuildSince(build, sinceIso) {
|
|
17345
|
+
const t = Date.parse(buildEventTime(build));
|
|
17346
|
+
const s = Date.parse(sinceIso);
|
|
17347
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
17348
|
+
return t > s;
|
|
17349
|
+
}
|
|
17350
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
17351
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
17352
|
+
if (!since) return floor.toISOString();
|
|
17353
|
+
const sinceMs = new Date(since).getTime();
|
|
17354
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
17355
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
17356
|
+
}
|
|
17357
|
+
var EasConnector = class {
|
|
17358
|
+
constructor(config, fetchImpl) {
|
|
17359
|
+
this.config = config;
|
|
17360
|
+
this.fetchImpl = fetchImpl;
|
|
17361
|
+
}
|
|
17362
|
+
config;
|
|
17363
|
+
fetchImpl;
|
|
17364
|
+
provider = "eas";
|
|
17365
|
+
async poll(ctx) {
|
|
17366
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
17367
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
17368
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
17369
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
17370
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
17371
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
17372
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
17373
|
+
for (const build of fresh) {
|
|
17374
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
17375
|
+
}
|
|
17376
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
17377
|
+
}
|
|
17378
|
+
};
|
|
17379
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
17380
|
+
return {
|
|
17381
|
+
connector: new EasConnector(config, fetchImpl),
|
|
17382
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
17383
|
+
};
|
|
17384
|
+
}
|
|
17385
|
+
|
|
16590
17386
|
// src/connectors/registry.ts
|
|
16591
17387
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
16592
17388
|
async function authProbe(input) {
|
|
@@ -16874,6 +17670,41 @@ var PROVIDER_DISPATCH = {
|
|
|
16874
17670
|
...fetchImpl ? { fetchImpl } : {}
|
|
16875
17671
|
});
|
|
16876
17672
|
}
|
|
17673
|
+
},
|
|
17674
|
+
eas: {
|
|
17675
|
+
provider: "eas",
|
|
17676
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
17677
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
17678
|
+
primaryCredentialKey: "token",
|
|
17679
|
+
requiredCredentialFields: ["token"],
|
|
17680
|
+
requiredOptionFields: ["appId"],
|
|
17681
|
+
build(graph, options) {
|
|
17682
|
+
return createEasConnector(graph, options);
|
|
17683
|
+
},
|
|
17684
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
17685
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
17686
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
17687
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
17688
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
17689
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
17690
|
+
// silently at the first poll.
|
|
17691
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
17692
|
+
const cfg = options;
|
|
17693
|
+
const appId = String(cfg.appId ?? "");
|
|
17694
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
17695
|
+
const probeConfig = {
|
|
17696
|
+
appId,
|
|
17697
|
+
pageSize: 1,
|
|
17698
|
+
maxPages: 1,
|
|
17699
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
17700
|
+
};
|
|
17701
|
+
try {
|
|
17702
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
17703
|
+
return { ok: true };
|
|
17704
|
+
} catch (err) {
|
|
17705
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
17706
|
+
}
|
|
17707
|
+
}
|
|
16877
17708
|
}
|
|
16878
17709
|
};
|
|
16879
17710
|
function vercelCredsFrom(credentials) {
|
|
@@ -17086,7 +17917,11 @@ async function startConnectorPolling(input) {
|
|
|
17086
17917
|
const stopFns = all.map(
|
|
17087
17918
|
(registration) => startConnectorPollLoop(
|
|
17088
17919
|
registration.connector,
|
|
17089
|
-
{
|
|
17920
|
+
{
|
|
17921
|
+
projectDir: input.projectDir,
|
|
17922
|
+
credentials: registration.credentials,
|
|
17923
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
17924
|
+
},
|
|
17090
17925
|
input.graph,
|
|
17091
17926
|
registration.resolveTarget,
|
|
17092
17927
|
{ intervalMs: registration.intervalMs, connectorId: registration.id }
|
|
@@ -17415,10 +18250,15 @@ function registerRoutes(scope, ctx) {
|
|
|
17415
18250
|
}
|
|
17416
18251
|
const reg = built.registration;
|
|
17417
18252
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
18253
|
+
const incidentsPath = errorsPathFor(proj);
|
|
17418
18254
|
try {
|
|
17419
18255
|
const result = await ctx.runPoll(
|
|
17420
18256
|
reg.connector,
|
|
17421
|
-
{
|
|
18257
|
+
{
|
|
18258
|
+
projectDir: proj.scanPath ?? "",
|
|
18259
|
+
credentials: reg.credentials,
|
|
18260
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
18261
|
+
},
|
|
17422
18262
|
proj.graph,
|
|
17423
18263
|
reg.resolveTarget
|
|
17424
18264
|
);
|
|
@@ -17488,6 +18328,34 @@ function registerRoutes(scope, ctx) {
|
|
|
17488
18328
|
}
|
|
17489
18329
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
17490
18330
|
});
|
|
18331
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
18332
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18333
|
+
if (!proj) return;
|
|
18334
|
+
const { nodeId } = req.params;
|
|
18335
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18336
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18337
|
+
}
|
|
18338
|
+
const direction = req.query.direction;
|
|
18339
|
+
if (direction !== "up" && direction !== "down") {
|
|
18340
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18341
|
+
}
|
|
18342
|
+
const epath = errorsPathFor(proj);
|
|
18343
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18344
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18345
|
+
});
|
|
18346
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
18347
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18348
|
+
if (!proj) return;
|
|
18349
|
+
const { a, b } = req.query;
|
|
18350
|
+
if (!a || !b) {
|
|
18351
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18352
|
+
}
|
|
18353
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
18354
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18355
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18356
|
+
}
|
|
18357
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18358
|
+
});
|
|
17491
18359
|
scope.get("/search", async (req, reply) => {
|
|
17492
18360
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
17493
18361
|
if (!proj) return;
|
|
@@ -17981,8 +18849,8 @@ export {
|
|
|
17981
18849
|
emitNeatEvent,
|
|
17982
18850
|
attachGraphToEventBus,
|
|
17983
18851
|
confidenceForEdge,
|
|
17984
|
-
getRootCause,
|
|
17985
18852
|
getBlastRadius,
|
|
18853
|
+
getRootCause,
|
|
17986
18854
|
evaluateAllPolicies,
|
|
17987
18855
|
loadPolicyFile,
|
|
17988
18856
|
PolicyViolationsLog,
|
|
@@ -18056,4 +18924,4 @@ export {
|
|
|
18056
18924
|
deprovisionConnector,
|
|
18057
18925
|
buildApi
|
|
18058
18926
|
};
|
|
18059
|
-
//# sourceMappingURL=chunk-
|
|
18927
|
+
//# sourceMappingURL=chunk-3PXRQH53.js.map
|