Skip to content

Conversation

@vitoUwu
Copy link
Contributor

@vitoUwu vitoUwu commented Sep 7, 2025

Summary by CodeRabbit

  • New Features
    • Per-request context propagation across async boundaries and richer request metadata (URL, method, path, user agent, correlation ID) available to handlers and telemetry.
  • Bug Fixes
    • None.
  • Refactor
    • Error logging standardized to structured format with contextual metadata without changing behavior or responses.

@coderabbitai
Copy link

coderabbitai bot commented Sep 7, 2025

Walkthrough

Adds per-request metadata propagation via AsyncLocalStorage, extends RequestContext with optional request fields, enriches OpenTelemetry logs with that context, updates loader cache error logging to structured objects, and reuses a single URL instance during request handling. No control-flow or return-value changes.

Changes

Cohort / File(s) Summary of changes
RequestContext type and runtime population
deco.ts, runtime/mod.ts
RequestContext gains optional fields: url, method, pathname, userAgent, correlationId. runtime/mod.ts creates a single URL from request.url, assigns it to state.url, and populates liveContext.request with these fields.
AsyncLocalStorage request context & middleware propagation
observability/otel/context.ts, runtime/middleware.ts
Adds requestContextStore: AsyncLocalStorage<RequestContext> and helper withRequestContext(context, fn). runtime/middleware.ts wraps downstream next() with withRequestContext(...) to run handlers inside the per-request store.
Logging enrichment
observability/otel/logger.ts
Logger extracts request context (via requestContextStore.getStore() or Context.active()?.request) and merges request.url, request.method, request.pathname, request.userAgent, request.correlationId into emitted log attributes; extraction is try/catch guarded.
Structured loader error logging
blocks/loader.ts
Replaces plain-text error logs in cache .catch(...) handlers with structured logs including loader, cacheKey, and error details (message, stack) alongside the original error text.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Middleware as runtime/middleware.ts
  participant Runtime as runtime/mod.ts
  participant ALS as observability/otel/context.ts
  participant Handler as downstream handlers

  Client->>Middleware: HTTP request
  Middleware->>Runtime: build state & liveContext
  Runtime->>ALS: withRequestContext({url,method,pathname,userAgent,correlationId}, fn)
  note right of ALS #e6f7ff: AsyncLocalStorage.run sets store
  ALS->>Handler: invoke next() (context available)
  Handler->>Handler: async operations (can access request context)
Loading
sequenceDiagram
  autonumber
  participant Logger as observability/otel/logger.ts
  participant ALS as observability/otel/context.ts
  participant Sink as Log Sink

  Logger->>ALS: requestContextStore.getStore() / Context.active()
  alt context present
    Logger->>Logger: merge request.* fields into attrs
  else
    Logger->>Logger: continue without request attrs
  end
  Logger->>Sink: emit(log, mergedAttrs)
Loading
sequenceDiagram
  autonumber
  participant Loader as blocks/loader.ts
  participant Cache as Cache
  participant Logger as observability/otel/logger.ts

  Loader->>Cache: cache.put(...)
  Cache-->>Loader: reject(error)
  Loader->>Logger: log({ loader, cacheKey, error: { message, stack } }, text)

  Loader->>Loader: stale-while-revalidate -> callHandlerAndCache()
  Loader-->>Loader: catch(error)
  Loader->>Logger: log({ loader, cacheKey, error: { message, stack } }, text)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I hop through logs with twinkling nose,
Carrying URL crumbs where the warm stream flows.
I tuck method and agent into my lair,
Correlation ribbon trailing in the air.
If the cache cries out, I note each track—structured, neat, and right on track. 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98adc18 and ec27f46.

📒 Files selected for processing (1)
  • runtime/middleware.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • runtime/middleware.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improve-loader-logs

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 7, 2025

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.120.13 update
  • 🎉 for Minor 1.121.0 update
  • 🚀 for Major 2.0.0 update

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
runtime/mod.ts (1)

171-199: Wrap the HTTP handler with a per‐request Context.bind
In runtime/handler.tsx (around line 173), instead of returning hono.fetch.bind(hono) directly, wrap it with Context.bind (or RequestContext.bind) and include a new request object (at minimum signal, framework, url, method, pathname, userAgent) so that Context.active() is isolated per incoming request.

observability/otel/logger.ts (1)

137-149: Flatten attributes; nested objects are not valid OTEL attribute values

Attributes should be flat primitives/arrays. The current request: { … } object risks being dropped or rejected by exporters.

Apply:

-    // Automatically enrich logs with request context
-    const requestContext = this.extractRequestContext();
+    // Automatically enrich logs with request context (flat attributes)
+    const requestContext = this.extractRequestContext();

And ensure extractRequestContext returns flat keys (see below).

🧹 Nitpick comments (2)
runtime/mod.ts (1)

230-235: Consider redacting URL query/PII before storing it in context

Storing the full URL may capture tokens/emails in query strings. Prefer pathname or a redacted URL.

Example:

-      url: request.url,
+      url: `${url.origin}${url.pathname}`, // no query/hash
deco.ts (1)

31-41: Additions look good; type nits for stronger contracts

Optional request fields are fine. Minor type tightenings:

Apply:

-  /** Request URL for context */
-  url?: string;
-  /** Request method */
-  method?: string;
-  /** Request pathname */
-  pathname?: string;
+  /** Full (possibly redacted) request URL */
+  url?: string;
+  /** Request method */
+  method?: Request["method"];
+  /** Request pathname (no query/hash) */
+  pathname?: string;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a51f87 and a07702a.

📒 Files selected for processing (4)
  • blocks/loader.ts (2 hunks)
  • deco.ts (1 hunks)
  • observability/otel/logger.ts (3 hunks)
  • runtime/mod.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
runtime/mod.ts (2)
runtime/middleware.ts (1)
  • state (40-42)
utils/http.ts (1)
  • defaultHeaders (142-144)
blocks/loader.ts (2)
observability/otel/config.ts (1)
  • logger (60-70)
observability/mod.ts (1)
  • logger (2-2)
observability/otel/logger.ts (1)
deco.ts (1)
  • Context (119-141)
🔇 Additional comments (1)
runtime/mod.ts (1)

242-242: LGTM: reuse a single URL object

Reusing url via state.url avoids repeated parsing and is a nice micro-optimization.

Comment on lines +316 to +325
).catch((error) =>
logger.error(`loader error ${error}`, {
loader,
cacheKey: cacheKeyValue,
error: {
message: error.message,
stack: error.stack,
},
})
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use OTEL exception semantic keys and flatten error attributes

Nested error objects in attributes are non-compliant. Flatten and align with exception.* keys. Also avoid duplicating the error details in both message and attributes if noisy.

Apply:

-          ).catch((error) =>
-            logger.error(`loader error ${error}`, {
-              loader,
-              cacheKey: cacheKeyValue,
-              error: {
-                message: error.message,
-                stack: error.stack,
-              },
-            })
-          );
+          ).catch((error) =>
+            logger.error(`loader cache.put failed`, {
+              loader,
+              cacheKey: cacheKeyValue,
+              "exception.type": error?.name,
+              "exception.message": error?.message,
+              "exception.stacktrace": error?.stack,
+              "event.name": "loader.cache.put.error",
+            })
+          );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
).catch((error) =>
logger.error(`loader error ${error}`, {
loader,
cacheKey: cacheKeyValue,
error: {
message: error.message,
stack: error.stack,
},
})
);
).catch((error) =>
logger.error(`loader cache.put failed`, {
loader,
cacheKey: cacheKeyValue,
"exception.type": error?.name,
"exception.message": error?.message,
"exception.stacktrace": error?.stack,
"event.name": "loader.cache.put.error",
})
);
🤖 Prompt for AI Agents
In blocks/loader.ts around lines 316 to 325, the current catch logs a nested
error object and duplicates details in the message; replace that with a
flattened set of OTEL exception semantic keys (exception.type,
exception.message, exception.stacktrace) and keep the log message concise (e.g.,
"loader error") while including loader and cacheKey attributes; ensure you
safely derive type/message/stack from the caught value (falling back to
typeof/error.toString() if needed) and do not include a nested error object in
the attributes.

Comment on lines +348 to 356
logger.error(`loader error ${error}`, {
loader,
cacheKey: cacheKeyValue,
error: {
message: error.message,
stack: error.stack,
},
})
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same here: flatten and standardize exception fields in revalidate path

Match the structure used above for consistency and exporter compatibility.

Apply:

-            callHandlerAndCache().catch((error) =>
-              logger.error(`loader error ${error}`, {
-                loader,
-                cacheKey: cacheKeyValue,
-                error: {
-                  message: error.message,
-                  stack: error.stack,
-                },
-              })
-            );
+            callHandlerAndCache().catch((error) =>
+              logger.error(`loader revalidate failed`, {
+                loader,
+                cacheKey: cacheKeyValue,
+                "exception.type": error?.name,
+                "exception.message": error?.message,
+                "exception.stacktrace": error?.stack,
+                "event.name": "loader.revalidate.error",
+              })
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.error(`loader error ${error}`, {
loader,
cacheKey: cacheKeyValue,
error: {
message: error.message,
stack: error.stack,
},
})
);
callHandlerAndCache().catch((error) =>
logger.error(`loader revalidate failed`, {
loader,
cacheKey: cacheKeyValue,
"exception.type": error?.name,
"exception.message": error?.message,
"exception.stacktrace": error?.stack,
"event.name": "loader.revalidate.error",
})
);
🤖 Prompt for AI Agents
In blocks/loader.ts around lines 348 to 356, the revalidate-path logger call
nests the error under an "error" object which differs from the standardized,
flattened exception shape used elsewhere; change the logger.error invocation to
use the same flattened fields (e.g.,
exception_name/exception_message/exception_stack or name/message/stack depending
on the project's convention) alongside loader and cacheKey, and remove the
nested error object so the payload matches the other logs and exporter
expectations.

Comment on lines 153 to 181
/**
* Extracts request context data from the current context
*/
private extractRequestContext(): Record<string, unknown> {
try {
const activeContext = Context.active();
const requestContext = activeContext.request;

return {
siteId: activeContext.siteId,
siteName: activeContext.site,
platform: activeContext.platform,
deploymentId: activeContext.deploymentId,
request: {
url: requestContext?.url,
method: requestContext?.method,
pathname: requestContext?.pathname,
userAgent: requestContext?.userAgent,
correlationId: requestContext?.correlationId,
framework: requestContext?.framework,
},
};
} catch {
console.error("Error extracting request context");
// If context is not available, return empty object
return {};
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return flat, spec-aligned keys (and default framework) from extractRequestContext

Use dot-delimited keys and OTEL-ish names; avoid nested objects. Also default framework to "fresh".

Apply:

-  private extractRequestContext(): Record<string, unknown> {
+  private extractRequestContext(): Record<string, unknown> {
     try {
       const activeContext = Context.active();
-      const requestContext = activeContext.request;
-
-      return {
-        siteId: activeContext.siteId,
-        siteName: activeContext.site,
-        platform: activeContext.platform,
-        deploymentId: activeContext.deploymentId,
-        request: {
-          url: requestContext?.url,
-          method: requestContext?.method,
-          pathname: requestContext?.pathname,
-          userAgent: requestContext?.userAgent,
-          correlationId: requestContext?.correlationId,
-          framework: requestContext?.framework,
-        },
-      };
+      const r = activeContext.request;
+      return {
+        "site.id": activeContext.siteId,
+        "site.name": activeContext.site,
+        "host.platform": activeContext.platform,
+        "deployment.id": activeContext.deploymentId,
+        "request.url": r?.url,
+        "request.method": r?.method,
+        "request.pathname": r?.pathname,
+        "user_agent.original": r?.userAgent,
+        "request.correlation_id": r?.correlationId,
+        "framework.name": r?.framework ?? "fresh",
+      };
     } catch {
       console.error("Error extracting request context");
       // If context is not available, return empty object
       return {};
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Extracts request context data from the current context
*/
private extractRequestContext(): Record<string, unknown> {
try {
const activeContext = Context.active();
const requestContext = activeContext.request;
return {
siteId: activeContext.siteId,
siteName: activeContext.site,
platform: activeContext.platform,
deploymentId: activeContext.deploymentId,
request: {
url: requestContext?.url,
method: requestContext?.method,
pathname: requestContext?.pathname,
userAgent: requestContext?.userAgent,
correlationId: requestContext?.correlationId,
framework: requestContext?.framework,
},
};
} catch {
console.error("Error extracting request context");
// If context is not available, return empty object
return {};
}
}
private extractRequestContext(): Record<string, unknown> {
try {
const activeContext = Context.active();
const r = activeContext.request;
return {
"site.id": activeContext.siteId,
"site.name": activeContext.site,
"host.platform": activeContext.platform,
"deployment.id": activeContext.deploymentId,
"request.url": r?.url,
"request.method": r?.method,
"request.pathname": r?.pathname,
"user_agent.original": r?.userAgent,
"request.correlation_id": r?.correlationId,
"framework.name": r?.framework ?? "fresh",
};
} catch {
console.error("Error extracting request context");
// If context is not available, return empty object
return {};
}
}
🤖 Prompt for AI Agents
In observability/otel/logger.ts around lines 153-181, extractRequestContext
currently returns a nested request object and non-spec keys; change it to return
a flat Record<string, unknown> using dot-delimited, OTEL-ish keys (no nested
objects) and default framework to "fresh" when missing. Specifically, map
context values to keys like "site.id", "site.name", "platform", "deployment.id",
"http.url", "http.method", "http.target" (or "http.pathname"),
"http.user_agent", "correlation.id" (or "trace.correlation_id"), and "framework"
(defaulting to "fresh"); ensure safe optional access to requestContext
properties and keep the try/catch behavior returning {} on error.

Comment on lines 223 to 236
const liveContext = this.ctx;
const request = forceHttps(req);
const url = new URL(request.url);

// Create a new request context
liveContext.request = {};

liveContext.request.url = request.url;
liveContext.request.method = request.method;
liveContext.request.pathname = url.pathname;
liveContext.request.userAgent = request.headers.get("user-agent") ||
undefined;
liveContext.request.correlationId = correlationId;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid mutating the shared context; use Context.active() to prevent cross-request leaks

this.ctx is the global/default context. Mutating liveContext.request here risks leaking request data across concurrent requests if AsyncLocalStorage isn’t bound with a per-request clone. Bind per-request context and/or read the active one before mutation.

Apply:

-    const liveContext = this.ctx;
+    const liveContext = Context.active();

And set the request context atomically to reduce intermediate inconsistent states:

-    // Create a new request context
-    liveContext.request = {};
-
-    liveContext.request.url = request.url;
-    liveContext.request.method = request.method;
-    liveContext.request.pathname = url.pathname;
-    liveContext.request.userAgent = request.headers.get("user-agent") ||
-      undefined;
-    liveContext.request.correlationId = correlationId;
+    liveContext.request = {
+      url: request.url,
+      method: request.method,
+      pathname: url.pathname,
+      userAgent: request.headers.get("user-agent") ?? undefined,
+      correlationId,
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const liveContext = this.ctx;
const request = forceHttps(req);
const url = new URL(request.url);
// Create a new request context
liveContext.request = {};
liveContext.request.url = request.url;
liveContext.request.method = request.method;
liveContext.request.pathname = url.pathname;
liveContext.request.userAgent = request.headers.get("user-agent") ||
undefined;
liveContext.request.correlationId = correlationId;
const liveContext = Context.active();
const request = forceHttps(req);
const url = new URL(request.url);
liveContext.request = {
url: request.url,
method: request.method,
pathname: url.pathname,
userAgent: request.headers.get("user-agent") ?? undefined,
correlationId,
};
🤖 Prompt for AI Agents
In runtime/mod.ts around lines 223 to 236, you are mutating the shared this.ctx
which can leak data across concurrent requests; instead obtain the per-request
context via Context.active() (or clone this.ctx into a fresh context tied to the
current request) and assign the request object in one atomic operation. Replace
the stepwise property assignments with creating a new request object (containing
url, method, pathname, userAgent, correlationId) and set it onto the per-request
context returned by Context.active() (or the cloned context) in a single
assignment so no intermediate mutated state is exposed to other requests.

…calStorage e atualizar logger para extrair dados do novo contexto
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
observability/otel/logger.ts (1)

154-174: Adopt OTEL-ish, flat attribute keys (align with spans) and default framework.

Keys like request.userAgent/request.pathname are non-standard and diverge from span attributes (http.request.*, url.path, user_agent.original). Aligning improves cross-signal correlation.

-  private extractRequestContext(): Record<string, unknown> {
+  private extractRequestContext(): Record<string, unknown> {
     try {
-      const context = requestContextStore.getStore() ||
-        Context.active()?.request;
-
-      return {
-        "request.url": context?.url,
-        "request.method": context?.method,
-        "request.pathname": context?.pathname,
-        "request.userAgent": context?.userAgent,
-        "request.correlationId": context?.correlationId,
-      };
+      const r = requestContextStore.getStore() || Context.active()?.request;
+      return {
+        "http.request.url": r?.url,
+        "http.request.method": r?.method,
+        "url.path": r?.pathname,
+        "user_agent.original": r?.userAgent,
+        "request.correlation_id": r?.correlationId,
+        "framework.name": "fresh",
+      };
     } catch (error) {
       console.error("Error extracting request context:", error);
       // If context is not available, return empty object
       return {};
     }
   }
🧹 Nitpick comments (1)
observability/otel/context.ts (1)

7-13: Export the interface and consider future-proofing fields.

Exporting RequestContext lets callers type their payloads; consider adding framework?: string if you intend to log it.

-interface RequestContext {
+export interface RequestContext {
   url?: string;
   method?: string;
   pathname?: string;
   userAgent?: string;
   correlationId?: string;
+  // framework?: string;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a07702a and 98adc18.

📒 Files selected for processing (3)
  • observability/otel/context.ts (1 hunks)
  • observability/otel/logger.ts (4 hunks)
  • runtime/middleware.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
runtime/middleware.ts (1)
observability/otel/context.ts (1)
  • withRequestContext (17-22)
observability/otel/logger.ts (2)
deco.ts (2)
  • context (188-188)
  • Context (119-141)
observability/otel/context.ts (1)
  • requestContextStore (15-15)
observability/otel/context.ts (1)
deps.ts (2)
  • createContextKey (12-12)
  • context (11-11)
🔇 Additional comments (7)
runtime/middleware.ts (1)

6-6: Import looks correct.

withRequestContext is used below; no issues.

observability/otel/logger.ts (3)

6-6: Import is appropriate.

Using requestContextStore keeps log enrichment decoupled from Deco.Context.


26-26: Good fallback to Context.active().

Keeps compatibility when ALS isn’t set.


138-150: Enrichment placement LGTM.

Merging extracted request attrs into attributes is correct. No functional concerns here.

observability/otel/context.ts (3)

15-15: Store definition LGTM.

Type aligns with the interface; no issues.


17-22: Wrapper LGTM.

withRequestContext correctly delegates to run and supports async fns via returned Promise.


1-1: Guard node:async_hooks import or enable Deno --compat
Deno tasks don’t include --compat, so import { AsyncLocalStorage } from "node:async_hooks" will fail in pure Deno; either run your CI/dev commands with --compat (or add it in deno.json tasks) or switch to std/node/async_hooks and provide a no-op fallback for non-Node runtimes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants