@farming-labs/theme 0.1.71 → 0.1.72

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.
@@ -1,4 +1,4 @@
1
- import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
1
+ import { ChangelogConfig, DocsAnalyticsConfig, DocsI18nConfig, DocsMcpConfig, DocsObservabilityConfig, DocsSearchConfig, FeedbackConfig, OrderingItem } from "@farming-labs/docs";
2
2
 
3
3
  //#region src/docs-api.d.ts
4
4
  interface AIProviderConfig {
@@ -43,6 +43,8 @@ interface DocsAPIOptions {
43
43
  search?: boolean | DocsSearchConfig;
44
44
  /** Analytics configuration */
45
45
  analytics?: boolean | DocsAnalyticsConfig;
46
+ /** Observability configuration for logs, traces, and metrics callbacks. */
47
+ observability?: boolean | DocsObservabilityConfig;
46
48
  /** Feedback configuration */
47
49
  feedback?: boolean | FeedbackConfig;
48
50
  /** MCP configuration used for the agent discovery spec. */
@@ -59,6 +61,7 @@ interface DocsMCPAPIOptions {
59
61
  mcp?: boolean | DocsMcpConfig;
60
62
  search?: boolean | DocsSearchConfig;
61
63
  analytics?: boolean | DocsAnalyticsConfig;
64
+ observability?: boolean | DocsObservabilityConfig;
62
65
  }
63
66
  /**
64
67
  * Create a unified docs API route handler.
package/dist/docs-api.mjs CHANGED
@@ -3,7 +3,7 @@ import { getNextAppDir } from "./get-app-dir.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import matter from "gray-matter";
6
- import { emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
6
+ import { createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, normalizeDocsRelated, performDocsSearch, renderDocsRelatedMarkdownLines, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig } from "@farming-labs/docs";
7
7
  import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource, resolveDocsMcpConfig } from "@farming-labs/docs/server";
8
8
 
9
9
  //#region src/docs-api.ts
@@ -956,9 +956,74 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
956
956
  apiKey
957
957
  };
958
958
  }
959
- async function handleAskAI(request, indexes, aiConfig, analytics, analyticsContext = {}) {
959
+ function safeUrlOrigin(value) {
960
+ try {
961
+ return new URL(value).origin;
962
+ } catch {
963
+ return value;
964
+ }
965
+ }
966
+ async function handleAskAI(request, indexes, aiConfig, analytics, observability, analyticsContext = {}) {
960
967
  const url = new URL(request.url);
961
968
  const requestStartedAt = Date.now();
969
+ const trace = createDocsAgentTraceContext("ask-ai");
970
+ const runSpanId = createDocsAgentTraceId("span");
971
+ const traceBase = {
972
+ source: "server",
973
+ traceId: trace.traceId,
974
+ url: request.url,
975
+ path: url.pathname,
976
+ locale: analyticsContext.locale
977
+ };
978
+ async function emitTrace(event) {
979
+ await emitDocsAgentTraceEvent(observability, {
980
+ ...traceBase,
981
+ ...event
982
+ });
983
+ }
984
+ async function emitRunError(reason, outputPreview = {}) {
985
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
986
+ const elapsed = Math.max(0, Date.now() - requestStartedAt);
987
+ const common = {
988
+ name: "ask-ai",
989
+ startedAt: trace.startedAt,
990
+ endedAt,
991
+ durationMs: elapsed,
992
+ status: "error",
993
+ outputPreview: {
994
+ reason,
995
+ ...outputPreview
996
+ },
997
+ metadata: { reason }
998
+ };
999
+ await emitTrace({
1000
+ ...common,
1001
+ type: "error",
1002
+ parentSpanId: runSpanId
1003
+ });
1004
+ await emitTrace({
1005
+ ...common,
1006
+ type: "run.error",
1007
+ spanId: runSpanId
1008
+ });
1009
+ await emitTrace({
1010
+ ...common,
1011
+ type: "run.end",
1012
+ spanId: runSpanId
1013
+ });
1014
+ }
1015
+ await emitTrace({
1016
+ type: "run.start",
1017
+ name: "ask-ai",
1018
+ spanId: runSpanId,
1019
+ startedAt: trace.startedAt,
1020
+ durationMs: 0,
1021
+ status: "started",
1022
+ inputPreview: {
1023
+ method: request.method,
1024
+ path: url.pathname
1025
+ }
1026
+ });
962
1027
  let body;
963
1028
  try {
964
1029
  body = await request.json();
@@ -974,6 +1039,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
974
1039
  durationMs: Math.max(0, Date.now() - requestStartedAt)
975
1040
  }
976
1041
  });
1042
+ await emitRunError("invalid_json", { status: 400 });
977
1043
  return Response.json({ error: "Invalid JSON body. Expected { messages: [...] }" }, { status: 400 });
978
1044
  }
979
1045
  const messages = body.messages;
@@ -989,6 +1055,7 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
989
1055
  durationMs: Math.max(0, Date.now() - requestStartedAt)
990
1056
  }
991
1057
  });
1058
+ await emitRunError("missing_messages", { status: 400 });
992
1059
  return Response.json({ error: "messages array is required and must not be empty." }, { status: 400 });
993
1060
  }
994
1061
  const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
@@ -1005,10 +1072,44 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1005
1072
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1006
1073
  }
1007
1074
  });
1075
+ await emitRunError("missing_user_message", {
1076
+ status: 400,
1077
+ messageCount: messages.length
1078
+ });
1008
1079
  return Response.json({ error: "At least one user message is required." }, { status: 400 });
1009
1080
  }
1010
1081
  const maxResults = aiConfig.maxResults ?? 5;
1011
1082
  const query = lastUserMessage.content;
1083
+ await emitTrace({
1084
+ type: "user.input",
1085
+ name: "ask-ai",
1086
+ parentSpanId: runSpanId,
1087
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1088
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1089
+ durationMs: 0,
1090
+ status: "success",
1091
+ inputPreview: {
1092
+ messageCount: messages.length,
1093
+ questionLength: query.length,
1094
+ requestedModel: typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0
1095
+ }
1096
+ });
1097
+ const retrievalStartedAt = Date.now();
1098
+ const retrievalStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1099
+ const retrievalSpanId = createDocsAgentTraceId("span");
1100
+ await emitTrace({
1101
+ type: "retrieval.query",
1102
+ name: "docs-index",
1103
+ spanId: retrievalSpanId,
1104
+ parentSpanId: runSpanId,
1105
+ startedAt: retrievalStartedAtIso,
1106
+ status: "started",
1107
+ inputPreview: {
1108
+ queryLength: query.length,
1109
+ maxResults,
1110
+ indexSize: indexes.length
1111
+ }
1112
+ });
1012
1113
  const scored = indexes.map((doc) => {
1013
1114
  const q = query.toLowerCase();
1014
1115
  const titleMatch = doc.title.toLowerCase().includes(q) ? 10 : 0;
@@ -1020,12 +1121,52 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1020
1121
  score: titleMatch + contentMatch
1021
1122
  };
1022
1123
  }).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults);
1124
+ await emitTrace({
1125
+ type: "retrieval.result",
1126
+ name: "docs-index",
1127
+ parentSpanId: retrievalSpanId,
1128
+ startedAt: retrievalStartedAtIso,
1129
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ durationMs: Math.max(0, Date.now() - retrievalStartedAt),
1131
+ status: "success",
1132
+ outputPreview: {
1133
+ resultCount: scored.length,
1134
+ urls: scored.slice(0, 5).map((doc) => doc.url)
1135
+ },
1136
+ metadata: {
1137
+ maxResults,
1138
+ indexSize: indexes.length
1139
+ }
1140
+ });
1141
+ const promptStartedAt = Date.now();
1142
+ const promptStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1143
+ const promptSpanId = createDocsAgentTraceId("span");
1023
1144
  const context = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`).join("\n\n---\n\n");
1024
1145
  const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1025
- const llmMessages = [{
1146
+ const systemMessage = {
1026
1147
  role: "system",
1027
1148
  content: context ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${context}` : systemPrompt
1028
- }, ...messages.filter((m) => m.role !== "system")];
1149
+ };
1150
+ const llmMessages = [systemMessage, ...messages.filter((m) => m.role !== "system")];
1151
+ await emitTrace({
1152
+ type: "prompt.build",
1153
+ name: "ask-ai.prompt",
1154
+ spanId: promptSpanId,
1155
+ parentSpanId: runSpanId,
1156
+ startedAt: promptStartedAtIso,
1157
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1158
+ durationMs: Math.max(0, Date.now() - promptStartedAt),
1159
+ status: "success",
1160
+ inputPreview: {
1161
+ messageCount: messages.length,
1162
+ retrievedCount: scored.length
1163
+ },
1164
+ outputPreview: {
1165
+ llmMessageCount: llmMessages.length,
1166
+ contextChars: context.length,
1167
+ systemMessageChars: systemMessage.content.length
1168
+ }
1169
+ });
1029
1170
  const resolved = resolveModelAndProvider(aiConfig, typeof body.model === "string" && body.model.trim().length > 0 ? body.model.trim() : void 0);
1030
1171
  if (!resolved.apiKey) {
1031
1172
  await emitDocsAnalyticsEvent(analytics, {
@@ -1044,6 +1185,13 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1044
1185
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1045
1186
  }
1046
1187
  });
1188
+ await emitRunError("missing_api_key", {
1189
+ status: 500,
1190
+ messageCount: messages.length,
1191
+ questionLength: query.length,
1192
+ retrievedCount: scored.length,
1193
+ model: resolved.model
1194
+ });
1047
1195
  return Response.json({ error: `AI is enabled but no API key was found. Either set apiKey in your docs.config ai section, configure a provider, or add OPENAI_API_KEY to your .env.local file.` }, { status: 500 });
1048
1196
  }
1049
1197
  await emitDocsAnalyticsEvent(analytics, {
@@ -1060,20 +1208,100 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1060
1208
  model: resolved.model
1061
1209
  }
1062
1210
  });
1063
- const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1064
- method: "POST",
1065
- headers: {
1066
- "Content-Type": "application/json",
1067
- Authorization: `Bearer ${resolved.apiKey}`
1068
- },
1069
- body: JSON.stringify({
1070
- model: resolved.model,
1211
+ const modelStartedAt = Date.now();
1212
+ const modelStartedAtIso = (/* @__PURE__ */ new Date()).toISOString();
1213
+ const modelSpanId = createDocsAgentTraceId("span");
1214
+ const providerOrigin = safeUrlOrigin(resolved.baseUrl);
1215
+ await emitTrace({
1216
+ type: "model.call",
1217
+ name: resolved.model,
1218
+ spanId: modelSpanId,
1219
+ parentSpanId: runSpanId,
1220
+ startedAt: modelStartedAtIso,
1221
+ status: "started",
1222
+ inputPreview: {
1223
+ messageCount: llmMessages.length,
1071
1224
  stream: true,
1072
- messages: llmMessages
1073
- })
1225
+ providerOrigin
1226
+ },
1227
+ metadata: { model: resolved.model }
1074
1228
  });
1229
+ let llmResponse;
1230
+ try {
1231
+ llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
1232
+ method: "POST",
1233
+ headers: {
1234
+ "Content-Type": "application/json",
1235
+ Authorization: `Bearer ${resolved.apiKey}`
1236
+ },
1237
+ body: JSON.stringify({
1238
+ model: resolved.model,
1239
+ stream: true,
1240
+ messages: llmMessages
1241
+ })
1242
+ });
1243
+ } catch (error) {
1244
+ const elapsed = Math.max(0, Date.now() - modelStartedAt);
1245
+ const message = error instanceof Error ? error.message : "Unknown error";
1246
+ await emitTrace({
1247
+ type: "model.error",
1248
+ name: resolved.model,
1249
+ parentSpanId: modelSpanId,
1250
+ startedAt: modelStartedAtIso,
1251
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1252
+ durationMs: elapsed,
1253
+ status: "error",
1254
+ outputPreview: { message },
1255
+ metadata: {
1256
+ model: resolved.model,
1257
+ providerOrigin
1258
+ }
1259
+ });
1260
+ await emitDocsAnalyticsEvent(analytics, {
1261
+ type: "api_ai_error",
1262
+ source: "server",
1263
+ url: request.url,
1264
+ path: url.pathname,
1265
+ locale: analyticsContext.locale,
1266
+ input: { question: query },
1267
+ properties: {
1268
+ reason: "llm_fetch_error",
1269
+ messageCount: messages.length,
1270
+ questionLength: query.length,
1271
+ retrievedCount: scored.length,
1272
+ model: resolved.model,
1273
+ durationMs: Math.max(0, Date.now() - requestStartedAt)
1274
+ }
1275
+ });
1276
+ await emitRunError("llm_fetch_error", {
1277
+ status: 502,
1278
+ messageCount: messages.length,
1279
+ questionLength: query.length,
1280
+ retrievedCount: scored.length,
1281
+ model: resolved.model
1282
+ });
1283
+ return Response.json({ error: "LLM API request failed." }, { status: 502 });
1284
+ }
1075
1285
  if (!llmResponse.ok) {
1076
1286
  const errText = await llmResponse.text().catch(() => "Unknown error");
1287
+ const elapsed = Math.max(0, Date.now() - modelStartedAt);
1288
+ await emitTrace({
1289
+ type: "model.error",
1290
+ name: resolved.model,
1291
+ parentSpanId: modelSpanId,
1292
+ startedAt: modelStartedAtIso,
1293
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1294
+ durationMs: elapsed,
1295
+ status: "error",
1296
+ outputPreview: {
1297
+ status: llmResponse.status,
1298
+ errorChars: errText.length
1299
+ },
1300
+ metadata: {
1301
+ model: resolved.model,
1302
+ providerOrigin
1303
+ }
1304
+ });
1077
1305
  await emitDocsAnalyticsEvent(analytics, {
1078
1306
  type: "api_ai_error",
1079
1307
  source: "server",
@@ -1091,6 +1319,14 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1091
1319
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1092
1320
  }
1093
1321
  });
1322
+ await emitRunError("llm_error", {
1323
+ status: 502,
1324
+ modelStatus: llmResponse.status,
1325
+ messageCount: messages.length,
1326
+ questionLength: query.length,
1327
+ retrievedCount: scored.length,
1328
+ model: resolved.model
1329
+ });
1094
1330
  return Response.json({ error: `LLM API error (${llmResponse.status}): ${errText}` }, { status: 502 });
1095
1331
  }
1096
1332
  await emitDocsAnalyticsEvent(analytics, {
@@ -1108,6 +1344,66 @@ async function handleAskAI(request, indexes, aiConfig, analytics, analyticsConte
1108
1344
  durationMs: Math.max(0, Date.now() - requestStartedAt)
1109
1345
  }
1110
1346
  });
1347
+ const responseEndedAt = (/* @__PURE__ */ new Date()).toISOString();
1348
+ const modelDurationMs = Math.max(0, Date.now() - modelStartedAt);
1349
+ await emitTrace({
1350
+ type: "model.response",
1351
+ name: resolved.model,
1352
+ parentSpanId: modelSpanId,
1353
+ startedAt: modelStartedAtIso,
1354
+ endedAt: responseEndedAt,
1355
+ durationMs: modelDurationMs,
1356
+ status: "success",
1357
+ outputPreview: {
1358
+ status: llmResponse.status,
1359
+ stream: true,
1360
+ contentType: llmResponse.headers.get("content-type") ?? void 0
1361
+ },
1362
+ metadata: {
1363
+ model: resolved.model,
1364
+ providerOrigin
1365
+ }
1366
+ });
1367
+ await emitTrace({
1368
+ type: "model.stream",
1369
+ name: resolved.model,
1370
+ parentSpanId: modelSpanId,
1371
+ startedAt: modelStartedAtIso,
1372
+ endedAt: responseEndedAt,
1373
+ durationMs: modelDurationMs,
1374
+ status: "success",
1375
+ outputPreview: { stream: true },
1376
+ metadata: { model: resolved.model }
1377
+ });
1378
+ const runDurationMs = Math.max(0, Date.now() - requestStartedAt);
1379
+ await emitTrace({
1380
+ type: "agent.final",
1381
+ name: "ask-ai",
1382
+ parentSpanId: runSpanId,
1383
+ startedAt: trace.startedAt,
1384
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1385
+ durationMs: runDurationMs,
1386
+ status: "success",
1387
+ outputPreview: {
1388
+ stream: true,
1389
+ retrievedCount: scored.length
1390
+ },
1391
+ metadata: { model: resolved.model }
1392
+ });
1393
+ await emitTrace({
1394
+ type: "run.end",
1395
+ name: "ask-ai",
1396
+ spanId: runSpanId,
1397
+ startedAt: trace.startedAt,
1398
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1399
+ durationMs: runDurationMs,
1400
+ status: "success",
1401
+ outputPreview: {
1402
+ stream: true,
1403
+ retrievedCount: scored.length
1404
+ },
1405
+ metadata: { model: resolved.model }
1406
+ });
1111
1407
  return new Response(llmResponse.body, { headers: {
1112
1408
  "Content-Type": "text/event-stream",
1113
1409
  "Cache-Control": "no-cache",
@@ -1188,6 +1484,7 @@ function createDocsAPI(options) {
1188
1484
  const root = options?.rootDir ?? process.cwd();
1189
1485
  const entry = options?.entry ?? readEntry(root);
1190
1486
  const analytics = options?.analytics;
1487
+ const observability = options?.observability;
1191
1488
  const appDir = getNextAppDir(root);
1192
1489
  const contentDir = options?.contentDir ?? path.join(appDir, entry);
1193
1490
  const changelogConfig = resolveChangelogConfig(options?.changelog);
@@ -1586,7 +1883,7 @@ function createDocsAPI(options) {
1586
1883
  return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
1587
1884
  }
1588
1885
  const ctx = resolveContextFromRequest(request);
1589
- return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, { locale: ctx.locale });
1886
+ return handleAskAI(request, getIndexes(ctx), aiConfig, analytics, observability, { locale: ctx.locale });
1590
1887
  }
1591
1888
  };
1592
1889
  }
@@ -1616,6 +1913,7 @@ function createDocsMCPAPI(options = {}) {
1616
1913
  mcp: options.mcp ?? readMcpConfig(rootDir),
1617
1914
  search: options.search,
1618
1915
  analytics: options.analytics,
1916
+ observability: options.observability,
1619
1917
  defaultName: navTitle
1620
1918
  });
1621
1919
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -139,7 +139,7 @@
139
139
  "tsdown": "^0.20.3",
140
140
  "typescript": "^5.9.3",
141
141
  "vitest": "^3.2.4",
142
- "@farming-labs/docs": "0.1.71"
142
+ "@farming-labs/docs": "0.1.72"
143
143
  },
144
144
  "peerDependencies": {
145
145
  "@farming-labs/docs": ">=0.0.1",