DEV Community

Roberto Luna
Roberto Luna

Posted on

Migrating Console Logging to Structured Observability & Adding Theme Support in CrmShellCondos (v2.0.0)

Migrating Console Logging to Structured Observability & Adding Theme Support in CrmShellCondos (v2.0.0)

TL;DR: I replaced every console.error/console.warn with a centralized reportError/reportWarning API across 14 backend files and added a fast‑follow theme import to CrmShellCondos.tsx. The changes give us consistent observability data and a cleaner way to toggle branding in the UI.


The Problem

Our monorepo was still littered with raw console.error and console.warn calls. In production on Vercel we could only see those messages in the serverless logs, which made it impossible to filter, enrich, or forward them to our monitoring stack (Grafana Loki + Loki‑push). The symptom was a flood of unstructured logs like:

ERROR: Unexpected token in query
WARN: PipelineAutomationService took 12s to complete
Enter fullscreen mode Exit fullscreen mode

At the same time, the CrmShellCondos component in the web app needed a quick “fast‑follow” to pick up the new branding theme (logo, colors) that the product team released for phase 2. The component was still importing the old static assets and the UI was not reflecting the new brand.


What I Tried First

1️⃣ Direct Replacement in Each File

My first instinct was to open every file that contained console.error or console.warn and replace the calls manually with reportError/reportWarning. I used a global search‑replace in VS Code:

search: console.error
replace: reportError
search: console.warn
replace: reportWarning
Enter fullscreen mode Exit fullscreen mode

What went wrong?

  • The replacement also touched comments and string literals, breaking documentation blocks.
  • Some files imported reportError already, causing duplicate import statements.
  • The new functions accept an optional metadata object; my blind replace passed only the original message, losing context.

2️⃣ Adding a Wrapper Around console

I then tried to create a tiny wrapper:

export const reportError = (msg: string) => console.error(msg);
export const reportWarning = (msg: string) => console.warn(msg);
Enter fullscreen mode Exit fullscreen mode

and import that wrapper everywhere. This kept the call signature identical, but it didn’t solve the core problem: we still weren’t sending structured data to our observability pipeline. The wrapper was essentially a no‑op.


The Implementation

1️⃣ Centralizing the Observability Helpers

I created a new module apps/api/src/shared/observability.ts:

// apps/api/src/shared/observability.ts
import { logger } from "./logger.js"; // our pino logger instance

export function reportError(message: string, meta?: Record<string, unknown>) {
  logger.error({ msg: message, ...meta });
}

export function reportWarning(message: string, meta?: Record<string, unknown>) {
  logger.warn({ msg: message, ...meta });
}
Enter fullscreen mode Exit fullscreen mode

The logger is a pre‑configured pino instance that already ships JSON to Loki. By funneling everything through reportError/reportWarning we get:

  • Consistent JSON shape ({msg, ...meta})
  • Automatic inclusion of request‑ids (via pino’s child logger)
  • Future ability to add tracing IDs without touching call sites.

2️⃣ Updating All 14 Files

I generated a diff that adds the import and replaces the calls with the new helpers. Below are two representative files.

apps/api/src/brokers/broker-portal.controller.ts

@@ -4,6 +4,7 @@ import { signAccessToken } from "../auth/security.js";
 import { jwtVerify } from "jose";
 import { env } from "../common/env.js";
 import { callGroq, getGroqKey } from "../shared/groq.helper.js";
+import { reportError, reportWarning } from "../shared/observability.js";

 // ...

- console.error("Failed to fetch broker data", err);
+ reportError("Failed to fetch broker data", { err, brokerId });
Enter fullscreen mode Exit fullscreen mode

apps/api/src/ventas/pipeline-automation.service.ts

@@ -12,6 +12,7 @@ import { query as db } from "../db/db.js";
 import { ClickUpService, CU_LISTS } from "../clickup/clickup.service.js";
 import { callGroq, getGroqKey } from "../shared/groq.helper.js";
+import { reportWarning } from "../shared/observability.js";

 // ...

- console.warn(`Pipeline took ${elapsed}s`);
+ reportWarning(`Pipeline took ${elapsed}s`, { elapsed });
Enter fullscreen mode Exit fullscreen mode

The diff shows the exact import line added and the call replacement. I ran the same transformation on the remaining 12 files using a small Node script that parses the AST with jscodeshift to avoid false positives in comments.

3️⃣ Fast‑Follow Theme Import in CrmShellCondos

The UI change was isolated to apps/web/src/app/_components/CrmShellCondos.tsx. The commit added a single import for the new theme utilities:

@@ -8,6 +8,7 @@ import { NavIcon } from "./navIcons";
 import { api } from "../../lib/api";
 import { useSession } from "../../lib/useSession";
 import { decodeJwt } from "../../lib/jwt";
+import { getCurrentTheme } from "../../lib/theme"; // <-- new import

 // ...

 const theme = getCurrentTheme(); // new line to fetch the brand config
Enter fullscreen mode Exit fullscreen mode

I also added a small hook to pull the theme from the backend:

// apps/web/src/lib/theme.ts
export async function getCurrentTheme() {
  const res = await fetch("/api/theme");
  if (!res.ok) throw new Error("Failed to load theme");
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

The component now reads the theme at render time and applies the new branding colors via a CSS module:

const CrmShellCondos = () => {
  const [theme, setTheme] = useState<any>(null);

  useEffect(() => {
    getCurrentTheme().then(setTheme).catch(console.error);
  }, []);

  if (!theme) return <Spinner />;
  return (
    <div className={styles.container} style={{ backgroundColor: theme.bg }}>
      <NavIcon src={theme.logo} alt="logo" />
      {/* …rest of UI… */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

4️⃣ Version Bump & Documentation

After the changes landed, I bumped the package version to v2.0.0 in package.json and updated the internal CLAUDE.md and CLAUDE_CODE_CONTEXT.md files to reflect the new build number (20260813). This keeps our AI‑assistant prompts in sync with the codebase.


Key Takeaway

Never replace logging ad‑hoc; always route through a single, structured observability layer. It gives you JSON logs, context propagation, and a single place to evolve the API (e.g., adding trace IDs) without hunting down every console.* call.


What's Next

  1. Integrate OpenTelemetry – attach trace IDs to reportError/reportWarning so we can correlate logs with distributed traces.
  2. Theme Caching – store the fetched theme in a React context to avoid a network call on every component mount.
  3. Automated Lint Rule – add an ESLint rule that flags any remaining console.error/

Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-08-14

#playadev #buildinpublic

Top comments (0)