From 3784e520b5efa25edba774645d3c55a51aba1748 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:28:47 +0530 Subject: [PATCH 01/12] feat: add errorStatus state to page context for error handling Handle non-ok responses from /api/page instead of silently catching. Track errorStatus as HTTP status code (number | null) to distinguish error states from loading state. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/page-context.tsx | 28 +++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/chronicle/src/lib/page-context.tsx b/packages/chronicle/src/lib/page-context.tsx index 2bb42323..20d3baa0 100644 --- a/packages/chronicle/src/lib/page-context.tsx +++ b/packages/chronicle/src/lib/page-context.tsx @@ -22,6 +22,7 @@ interface PageContextValue { config: ChronicleConfig; tree: Root; page: PageData | null; + errorStatus: number | null; apiSpecs: ApiSpec[]; } @@ -35,6 +36,7 @@ export function usePageContext(): PageContextValue { config: { title: 'Documentation' }, tree: { name: 'root', children: [] } as Root, page: null, + errorStatus: null, apiSpecs: [] }; } @@ -61,6 +63,7 @@ export function PageProvider({ const { pathname } = useLocation(); const [tree] = useState(initialTree); const [page, setPage] = useState(initialPage); + const [errorStatus, setErrorStatus] = useState(null); const [apiSpecs, setApiSpecs] = useState(initialApiSpecs); const [currentPath, setCurrentPath] = useState(pathname); @@ -89,21 +92,36 @@ export function PageProvider({ const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`; fetch(apiPath) - .then(res => res.json()) - .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string }) => { - if (cancelled.current) return; + .then(res => { + if (!res.ok) { + if (!cancelled.current) { + setPage(null); + setErrorStatus(res.status); + } + return; + } + return res.json(); + }) + .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string } | undefined) => { + if (cancelled.current || !data) return; const { content, toc } = await loadMdx(data.originalPath || data.relativePath); if (cancelled.current) return; + setErrorStatus(null); setPage({ slug, frontmatter: data.frontmatter, content, toc }); }) - .catch(() => {}); + .catch(() => { + if (!cancelled.current) { + setPage(null); + setErrorStatus(500); + } + }); return () => { cancelled.current = true; }; }, [pathname]); return ( {children} From 2547c36847c6ef6626938bd6bbcce920188ec24b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:32:19 +0530 Subject: [PATCH 02/12] feat: show 404 page when page API returns not found Wire existing NotFound component into DocsPage using errorStatus from page context. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/DocsPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index 3ebb7928..f308f5aa 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -1,5 +1,6 @@ import { Head } from '@/lib/head'; import { usePageContext } from '@/lib/page-context'; +import { NotFound } from '@/pages/NotFound'; import { getTheme } from '@/themes/registry'; interface DocsPageProps { @@ -7,8 +8,9 @@ interface DocsPageProps { } export function DocsPage({ slug }: DocsPageProps) { - const { config, tree, page } = usePageContext(); + const { config, tree, page, errorStatus } = usePageContext(); + if (errorStatus === 404) return ; if (!page) return null; const { Page } = getTheme(config.theme?.name); From 030e7389e94f9f1a86f59eab30d77305f5ebd149 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:42:01 +0530 Subject: [PATCH 03/12] feat: derive initial errorStatus for SSR 404 rendering Add getInitialErrorStatus() to compute error state from initialPage and pathname, so SSR also shows NotFound component on missing pages. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/page-context.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/chronicle/src/lib/page-context.tsx b/packages/chronicle/src/lib/page-context.tsx index 20d3baa0..ac7283e0 100644 --- a/packages/chronicle/src/lib/page-context.tsx +++ b/packages/chronicle/src/lib/page-context.tsx @@ -52,6 +52,12 @@ interface PageProviderProps { children: ReactNode; } +function getInitialErrorStatus(page: PageData | null, pathname: string): number | null { + if (page) return null; + if (pathname === '/' || pathname.startsWith('/apis')) return null; + return 404; +} + export function PageProvider({ initialConfig, initialTree, @@ -63,7 +69,7 @@ export function PageProvider({ const { pathname } = useLocation(); const [tree] = useState(initialTree); const [page, setPage] = useState(initialPage); - const [errorStatus, setErrorStatus] = useState(null); + const [errorStatus, setErrorStatus] = useState(getInitialErrorStatus(initialPage, pathname)); const [apiSpecs, setApiSpecs] = useState(initialApiSpecs); const [currentPath, setCurrentPath] = useState(pathname); From 53574b9fd1281cfa8a768469b9bced9d3a785c7d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:45:18 +0530 Subject: [PATCH 04/12] refactor: use Apsara EmptyState in NotFound page Replace manual Flex/Headline/Text layout with Apsara EmptyState component for consistent empty state styling. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/NotFound.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/chronicle/src/pages/NotFound.tsx b/packages/chronicle/src/pages/NotFound.tsx index ab92aabf..5781c04e 100644 --- a/packages/chronicle/src/pages/NotFound.tsx +++ b/packages/chronicle/src/pages/NotFound.tsx @@ -1,17 +1,10 @@ -import { Flex, Headline, Text } from '@raystack/apsara'; +import { EmptyState } from '@raystack/apsara'; export function NotFound() { return ( - - - 404 - - Page not found - + ); } From bb734e162d2b871dab114487921317e4c2f6d752 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:53:26 +0530 Subject: [PATCH 05/12] style: center NotFound EmptyState via classNames Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/pages/NotFound.module.css | 3 +++ packages/chronicle/src/pages/NotFound.tsx | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 packages/chronicle/src/pages/NotFound.module.css diff --git a/packages/chronicle/src/pages/NotFound.module.css b/packages/chronicle/src/pages/NotFound.module.css new file mode 100644 index 00000000..52283099 --- /dev/null +++ b/packages/chronicle/src/pages/NotFound.module.css @@ -0,0 +1,3 @@ +.emptyState { + justify-content: center; +} diff --git a/packages/chronicle/src/pages/NotFound.tsx b/packages/chronicle/src/pages/NotFound.tsx index 5781c04e..8cea1ade 100644 --- a/packages/chronicle/src/pages/NotFound.tsx +++ b/packages/chronicle/src/pages/NotFound.tsx @@ -1,10 +1,12 @@ import { EmptyState } from '@raystack/apsara'; +import styles from './NotFound.module.css'; export function NotFound() { return ( ); } From ef09ec47333a629eee657d099871cf22f1ec2e8d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 15:56:06 +0530 Subject: [PATCH 06/12] feat: serve metrics on separate port instead of /api/metrics PrometheusExporter now starts its own HTTP server on configurable port (default 9090). Removes /api/metrics route and mock request handler. Adds telemetry.port to config schema. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/api/metrics.ts | 23 ------------------- .../chronicle/src/server/plugins/telemetry.ts | 1 - packages/chronicle/src/server/telemetry.ts | 7 ++---- packages/chronicle/src/types/config.ts | 1 + 4 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 packages/chronicle/src/server/api/metrics.ts diff --git a/packages/chronicle/src/server/api/metrics.ts b/packages/chronicle/src/server/api/metrics.ts deleted file mode 100644 index 57b1d789..00000000 --- a/packages/chronicle/src/server/api/metrics.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'node:http' -import { defineHandler } from 'nitro' -import { getExporter } from '../telemetry' - -export default defineHandler(async () => { - const exporter = getExporter() - if (!exporter) { - return new Response('Telemetry not enabled', { status: 404 }) - } - - const metricsString = await new Promise((resolve) => { - const mockRes = { - setHeader: () => mockRes, - end: (data: string) => resolve(data), - } as unknown as ServerResponse - - exporter.getMetricsRequestHandler({} as unknown as IncomingMessage, mockRes) - }) - - return new Response(metricsString, { - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }) -}) diff --git a/packages/chronicle/src/server/plugins/telemetry.ts b/packages/chronicle/src/server/plugins/telemetry.ts index e5189f27..f1ccb474 100644 --- a/packages/chronicle/src/server/plugins/telemetry.ts +++ b/packages/chronicle/src/server/plugins/telemetry.ts @@ -9,7 +9,6 @@ export default definePlugin((nitroApp) => { initTelemetry(config) nitroApp.hooks.hook('request', (event) => { - if (event.path === '/api/metrics') return event.context._requestStart = performance.now() }) diff --git a/packages/chronicle/src/server/telemetry.ts b/packages/chronicle/src/server/telemetry.ts index 77c45635..443032de 100644 --- a/packages/chronicle/src/server/telemetry.ts +++ b/packages/chronicle/src/server/telemetry.ts @@ -20,7 +20,8 @@ export function initTelemetry(config: ChronicleConfig) { [ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle', }) - exporter = new PrometheusExporter({ preventServerStart: true }) + const port = config.telemetry?.port ?? 9090 + exporter = new PrometheusExporter({ port }) const provider = new MeterProvider({ resource, readers: [exporter] }) const meter = provider.getMeter('chronicle') @@ -35,10 +36,6 @@ export function initTelemetry(config: ChronicleConfig) { }) } -export function getExporter() { - return exporter -} - export function recordRequest(method: string, route: string, status: number, durationMs: number) { requestCounter?.add(1, { method, route, status }) requestDuration?.record(durationMs, { method, route, status }) diff --git a/packages/chronicle/src/types/config.ts b/packages/chronicle/src/types/config.ts index 59597221..68177d53 100644 --- a/packages/chronicle/src/types/config.ts +++ b/packages/chronicle/src/types/config.ts @@ -70,6 +70,7 @@ const analyticsSchema = z.object({ const telemetrySchema = z.object({ enabled: z.boolean().optional(), serviceName: z.string().optional(), + port: z.number().optional().default(9090), }) export const chronicleConfigSchema = z.object({ From 2e02f182bcfc3c1bae79811875c15194b755619d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 16:06:21 +0530 Subject: [PATCH 07/12] docs: add telemetry section to configuration reference Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/configuration.mdx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 33432afa..1c8ccb7d 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -62,6 +62,11 @@ analytics: enabled: true googleAnalytics: measurementId: G-XXXXXXXXXX + +telemetry: + enabled: true + serviceName: my-docs + port: 9090 ``` ## Reference @@ -267,6 +272,25 @@ analytics: | `enabled` | `boolean` | Enable/disable analytics | `false` | | `googleAnalytics.measurementId` | `string` | Google Analytics measurement ID | — | +### telemetry + +Prometheus metrics export via OpenTelemetry. When enabled, metrics are served on a separate port. + +```yaml +telemetry: + enabled: true + serviceName: my-docs + port: 9090 +``` + +| Field | Type | Description | Default | +|-------|------|-------------|---------| +| `enabled` | `boolean` | Enable/disable telemetry | `false` | +| `serviceName` | `string` | OpenTelemetry service name | `chronicle` | +| `port` | `number` | Port for Prometheus metrics endpoint | `9090` | + +Metrics are available at `http://localhost:/metrics` in Prometheus exposition format. + ## Defaults If `chronicle.yaml` is missing or fields are omitted, these defaults apply: From 3f5caaaec7775eeebe505ff1ec39a2f39ea0310e Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 16:08:12 +0530 Subject: [PATCH 08/12] fix: use named imports for OpenTelemetry packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default imports fail in ESM — these packages only export named. Fixes build failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/telemetry.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/chronicle/src/server/telemetry.ts b/packages/chronicle/src/server/telemetry.ts index 443032de..70075306 100644 --- a/packages/chronicle/src/server/telemetry.ts +++ b/packages/chronicle/src/server/telemetry.ts @@ -1,15 +1,10 @@ import type { Counter, Histogram } from '@opentelemetry/api' -import sdkMetrics from '@opentelemetry/sdk-metrics' -import prometheusExporter from '@opentelemetry/exporter-prometheus' -import resources from '@opentelemetry/resources' -import semconv from '@opentelemetry/semantic-conventions' +import { MeterProvider } from '@opentelemetry/sdk-metrics' +import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' import type { ChronicleConfig } from '@/types/config' -const { MeterProvider } = sdkMetrics -const { PrometheusExporter } = prometheusExporter -const { resourceFromAttributes } = resources -const { ATTR_SERVICE_NAME } = semconv - let exporter: PrometheusExporter let requestCounter: Counter let requestDuration: Histogram From 004da0fb4d3c1f637355e868d9c65fdf70bf1caa Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 14 Apr 2026 16:31:26 +0530 Subject: [PATCH 09/12] fix: address PR review comments - Tighten /apis route matching to avoid false positives on /apis-foo - Handle non-404 errors in DocsPage to avoid blank screen - Validate telemetry.port as int in 1-65535 range Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/page-context.tsx | 8 ++++++-- packages/chronicle/src/pages/DocsPage.tsx | 1 + packages/chronicle/src/types/config.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/chronicle/src/lib/page-context.tsx b/packages/chronicle/src/lib/page-context.tsx index ac7283e0..0252688d 100644 --- a/packages/chronicle/src/lib/page-context.tsx +++ b/packages/chronicle/src/lib/page-context.tsx @@ -52,9 +52,13 @@ interface PageProviderProps { children: ReactNode; } +function isApisRoute(pathname: string): boolean { + return pathname === '/apis' || pathname.startsWith('/apis/'); +} + function getInitialErrorStatus(page: PageData | null, pathname: string): number | null { if (page) return null; - if (pathname === '/' || pathname.startsWith('/apis')) return null; + if (pathname === '/' || isApisRoute(pathname)) return null; return 404; } @@ -79,7 +83,7 @@ export function PageProvider({ const cancelled = { current: false }; - if (pathname.startsWith('/apis')) { + if (isApisRoute(pathname)) { if (apiSpecs.length === 0) { fetch('/api/specs') .then(res => res.json()) diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index f308f5aa..e1710b49 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -11,6 +11,7 @@ export function DocsPage({ slug }: DocsPageProps) { const { config, tree, page, errorStatus } = usePageContext(); if (errorStatus === 404) return ; + if (errorStatus) return ; if (!page) return null; const { Page } = getTheme(config.theme?.name); diff --git a/packages/chronicle/src/types/config.ts b/packages/chronicle/src/types/config.ts index 68177d53..f28a322d 100644 --- a/packages/chronicle/src/types/config.ts +++ b/packages/chronicle/src/types/config.ts @@ -70,7 +70,7 @@ const analyticsSchema = z.object({ const telemetrySchema = z.object({ enabled: z.boolean().optional(), serviceName: z.string().optional(), - port: z.number().optional().default(9090), + port: z.number().int().min(1).max(65535).default(9090), }) export const chronicleConfigSchema = z.object({ From a00168e32db19d859a4c01ba8dd93a1de5421320 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 15 Apr 2026 14:48:15 +0530 Subject: [PATCH 10/12] refactor: consolidate telemetry into Nitro plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move OTel setup (MeterProvider, PrometheusExporter, counters/histograms) into plugins/telemetry.ts. Add custom 'chronicle:ssr-rendered' Nitro hook so entry-server can record SSR metrics via useNitroApp().hooks. Removes separate telemetry.ts module — all state lives in plugin scope. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/server/entry-server.tsx | 4 +- .../chronicle/src/server/plugins/telemetry.ts | 43 +++++++++++++++++-- packages/chronicle/src/server/telemetry.ts | 41 ------------------ 3 files changed, 42 insertions(+), 46 deletions(-) delete mode 100644 packages/chronicle/src/server/telemetry.ts diff --git a/packages/chronicle/src/server/entry-server.tsx b/packages/chronicle/src/server/entry-server.tsx index 0ccd49bd..fe23e1a9 100644 --- a/packages/chronicle/src/server/entry-server.tsx +++ b/packages/chronicle/src/server/entry-server.tsx @@ -9,8 +9,8 @@ import { loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; import { PageProvider } from '@/lib/page-context'; import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; +import { useNitroApp } from 'nitro/app'; import { App } from './App'; -import { recordSSRRender } from './telemetry'; import clientAssets from './entry-client?assets=client'; import serverAssets from './entry-server?assets=ssr'; @@ -98,7 +98,7 @@ export default { const isApiRoute = pathname.startsWith('/apis'); const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200; - recordSSRRender(pathname, status, renderDuration); + useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration); return new Response(stream, { status, diff --git a/packages/chronicle/src/server/plugins/telemetry.ts b/packages/chronicle/src/server/plugins/telemetry.ts index f1ccb474..b7cb9da4 100644 --- a/packages/chronicle/src/server/plugins/telemetry.ts +++ b/packages/chronicle/src/server/plugins/telemetry.ts @@ -1,12 +1,48 @@ +import type { Counter, Histogram } from '@opentelemetry/api' +import { MeterProvider } from '@opentelemetry/sdk-metrics' +import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' import { definePlugin } from 'nitro' import { loadConfig } from '@/lib/config' -import { initTelemetry, recordRequest } from '../telemetry' + +declare module 'nitro/types' { + interface NitroRuntimeHooks { + 'chronicle:ssr-rendered': (route: string, status: number, durationMs: number) => void + } +} export default definePlugin((nitroApp) => { const config = loadConfig() if (!config.telemetry?.enabled) return - initTelemetry(config) + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle', + }) + + const port = config.telemetry?.port ?? 9090 + const exporter = new PrometheusExporter({ port }) + const provider = new MeterProvider({ resource, readers: [exporter] }) + const meter = provider.getMeter('chronicle') + + const requestCounter: Counter = meter.createCounter('http_server_request_total', { + description: 'Total HTTP requests', + }) + const requestDuration: Histogram = meter.createHistogram('http_server_request_duration_ms', { + description: 'HTTP request duration in ms', + }) + const ssrRenderDuration: Histogram = meter.createHistogram('http_server_ssr_render_duration_ms', { + description: 'SSR render duration in ms', + }) + + nitroApp.hooks.hook('close', async () => { + await provider.shutdown() + await exporter.shutdown() + }) + + nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => { + ssrRenderDuration.record(durationMs, { route, status }) + }) nitroApp.hooks.hook('request', (event) => { event.context._requestStart = performance.now() @@ -15,6 +51,7 @@ export default definePlugin((nitroApp) => { nitroApp.hooks.hook('response', (res, event) => { if (!event.context._requestStart) return const duration = performance.now() - event.context._requestStart - recordRequest(event.method, event.path, res.status, duration) + requestCounter.add(1, { method: event.method, route: event.path, status: res.status }) + requestDuration.record(duration, { method: event.method, route: event.path, status: res.status }) }) }) diff --git a/packages/chronicle/src/server/telemetry.ts b/packages/chronicle/src/server/telemetry.ts deleted file mode 100644 index 70075306..00000000 --- a/packages/chronicle/src/server/telemetry.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Counter, Histogram } from '@opentelemetry/api' -import { MeterProvider } from '@opentelemetry/sdk-metrics' -import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' -import { resourceFromAttributes } from '@opentelemetry/resources' -import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' -import type { ChronicleConfig } from '@/types/config' - -let exporter: PrometheusExporter -let requestCounter: Counter -let requestDuration: Histogram -let ssrRenderDuration: Histogram - -export function initTelemetry(config: ChronicleConfig) { - const resource = resourceFromAttributes({ - [ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle', - }) - - const port = config.telemetry?.port ?? 9090 - exporter = new PrometheusExporter({ port }) - const provider = new MeterProvider({ resource, readers: [exporter] }) - const meter = provider.getMeter('chronicle') - - requestCounter = meter.createCounter('http_server_request_total', { - description: 'Total HTTP requests', - }) - requestDuration = meter.createHistogram('http_server_request_duration_ms', { - description: 'HTTP request duration in ms', - }) - ssrRenderDuration = meter.createHistogram('http_server_ssr_render_duration_ms', { - description: 'SSR render duration in ms', - }) -} - -export function recordRequest(method: string, route: string, status: number, durationMs: number) { - requestCounter?.add(1, { method, route, status }) - requestDuration?.record(durationMs, { method, route, status }) -} - -export function recordSSRRender(route: string, status: number, durationMs: number) { - ssrRenderDuration?.record(durationMs, { route, status }) -} From adb4046d5fe32e086aaf28af9aab6b0ace6baf06 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 15 Apr 2026 14:48:21 +0530 Subject: [PATCH 11/12] chore: enable telemetry in docs and add start:docs script Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/chronicle.yaml | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/docs/chronicle.yaml b/docs/chronicle.yaml index 045b114a..5a3de821 100644 --- a/docs/chronicle.yaml +++ b/docs/chronicle.yaml @@ -17,6 +17,9 @@ search: llms: enabled: true +telemetry: + enabled: true + footer: copyright: "© 2026 Raystack. All rights reserved." links: diff --git a/package.json b/package.json index c2516266..6421a8f4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "build:cli": "bun run --filter @raystack/chronicle build:cli", "dev:docs": "./packages/chronicle/bin/chronicle.js dev --config docs/chronicle.yaml", + "start:docs": "./packages/chronicle/bin/chronicle.js start --config docs/chronicle.yaml", "build:docs": "./packages/chronicle/bin/chronicle.js build --config docs/chronicle.yaml" } } From e422e7c0015aeb48587c86990461832d6607ee29 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 15 Apr 2026 15:50:51 +0530 Subject: [PATCH 12/12] fix: cast event to H3Event for context access in telemetry plugin NitroRuntimeHooks types event as HTTPEvent (only has req), but H3 runtime passes H3Event which has context, url, etc. Cast safely since runtime is always H3Event. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/server/plugins/telemetry.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/chronicle/src/server/plugins/telemetry.ts b/packages/chronicle/src/server/plugins/telemetry.ts index b7cb9da4..7f31d5fd 100644 --- a/packages/chronicle/src/server/plugins/telemetry.ts +++ b/packages/chronicle/src/server/plugins/telemetry.ts @@ -3,6 +3,7 @@ import { MeterProvider } from '@opentelemetry/sdk-metrics' import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' import { resourceFromAttributes } from '@opentelemetry/resources' import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' +import type { H3Event } from 'h3' import { definePlugin } from 'nitro' import { loadConfig } from '@/lib/config' @@ -45,13 +46,16 @@ export default definePlugin((nitroApp) => { }) nitroApp.hooks.hook('request', (event) => { - event.context._requestStart = performance.now() + (event as H3Event).context._requestStart = performance.now() }) nitroApp.hooks.hook('response', (res, event) => { - if (!event.context._requestStart) return - const duration = performance.now() - event.context._requestStart - requestCounter.add(1, { method: event.method, route: event.path, status: res.status }) - requestDuration.record(duration, { method: event.method, route: event.path, status: res.status }) + const start = (event as H3Event).context._requestStart as number | undefined + if (start === undefined) return + const duration = performance.now() - start + const method = event.req.method + const route = new URL(event.req.url).pathname + requestCounter.add(1, { method, route, status: res.status }) + requestDuration.record(duration, { method, route, status: res.status }) }) })