Skip to content

fix: ensure consoleLogLevel="silent" suppresses all SDK console output#1586

Open
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1771365994-fix-silent-log-level
Open

fix: ensure consoleLogLevel="silent" suppresses all SDK console output#1586
devin-ai-integration[bot] wants to merge 3 commits intomainfrom
devin/1771365994-fix-silent-log-level

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 17, 2026

Description

When using consoleLogLevel="silent" on CrossmintProvider, only logs routed through SdkLogger were silenced. Direct console.* calls scattered across auth, wallets, window, and React provider packages bypassed the setting entirely — so users still saw logs after login (iframe init, handshake, auth refresh, etc.).

This PR introduces a lightweight logToConsole utility in common-sdk-base that checks a global console log level before forwarding to native console.*. The ConsoleSink constructor syncs this global level whenever it's instantiated. All direct console.* calls in SDK packages are replaced with either logToConsole.* or the package's own SdkLogger instance.

Reported by @xmint-guilleLink to Devin run

Test plan

  • Full pnpm build:libs passes locally
  • pnpm lint passes
  • pnpm test:vitest passes — updated the existing CrossmintAuthClient.test.ts test that was spying on console.error to spy on logToConsole.error instead
  • Manual verification with the reporter's code snippet is recommended to confirm end-to-end silent behavior after login

Human review checklist

  • @crossmint/common-sdk-base added as dep to client-sdk-window: Window package was previously lightweight (only zod). common-sdk-base brings transitive deps (viem, @solana/web3.js). Evaluate bundle size impact vs. alternative approaches (injectable logger, standalone micro-module).
  • Global mutable state + ordering: logToConsole relies on setGlobalConsoleLogLevel being called (via ConsoleSink constructor) before any logToConsole.* usage. Default is "debug" (permissive), so early calls before provider init will still log. Verify this timing holds in all entry points.
  • Remaining raw console.* calls: SdkLogger.ts still has fallback console.* calls (lines 153, 378, 400, 429) for uninitialized-logger and sink-error paths. ConsoleProvider.ts in client/base also still uses raw console. These are intentionally left as-is but may still surface in some flows.

Package updates

Changeset added via .changeset/fix-silent-console-log-level.md covering patch bumps for:

  • @crossmint/common-sdk-base
  • @crossmint/client-sdk-window
  • @crossmint/client-sdk-auth
  • @crossmint/client-sdk-react-base
  • @crossmint/client-sdk-react-ui
  • @crossmint/wallets-sdk

Replace direct console.* calls across SDK packages with logToConsole
utility or package-specific SdkLogger instances. Previously, only logs
routed through SdkLogger were silenced by consoleLogLevel; direct
console calls in auth, wallets, window, and React provider code
bypassed the setting.

- Add logToConsole utility in common-sdk-base that respects global level
- Update ConsoleSink to set global console log level on construction
- Replace console.* in wallets package with walletsLogger
- Replace console.* in window, auth, react-base, react-ui with logToConsole
@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2026

🦋 Changeset detected

Latest commit: ef314c4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@crossmint/common-sdk-base Patch
@crossmint/client-sdk-window Patch
@crossmint/client-sdk-auth Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/wallets-sdk Patch
@crossmint/client-sdk-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-verifiable-credentials Patch
@crossmint/client-sdk-smart-wallet Patch
@crossmint/client-sdk-walletconnect Patch
@crossmint/common-sdk-auth Patch
@crossmint/server-sdk Patch
@crossmint/client-sdk-rn-window Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/client-sdk-smart-wallet-next-starter Patch
expo-demo Patch
crossmint-auth-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration
Copy link
Contributor Author

Original prompt from Guille
we introduced the following change here to supress logs in our sdks:
https://github.com/Crossmint/crossmint-sdk/commit/27194e5751fd4bb3afb190550382760f978d7a20

i've tried this with the following code snippet, and I can still see logs once I'm logged in:
```ts
"use client";

import {
  CrossmintProvider,
  CrossmintAuthProvider,
  CrossmintWalletProvider,
  useAuth,
  useWallet,
} from "@crossmint/client-sdk-react-ui";

function WalletUser() {
  const { login, user } = useAuth();
  const { wallet, status } = useWallet();

  return (
    <div>
      {user == null ? (
        <button onClick={login}>Sign In</button>
      ) : (
        <p>
          Wallet: {wallet?.address} ({status})
        </p>
      )}
    </div>
  );
}

export default function Page() {
  return (
    <CrossmintProvider
      apiKey={process.env.NEXT_PUBLIC_CLIENT_API_KEY ?? ""}
      consoleLogLevel="silent"
    >
      <CrossmintAuthProvider>
        <CrossmintWalletProvider
          createOnLogin={{
            chain: "polygon-amoy",
            signer: { type: "email" },
          }}
        >
          <WalletUser />
        </CrossmintWalletProvider>
      </CrossmintAuthProvider>
    </CrossmintProvider>
  );
}

can you determine the issue and propose a PR in case it's feasible to maintain the silent logs flag in every part of our sdks?

</details>

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Contributor

🔥 Smoke Test Results

Status: Passed

Statistics

  • Total Tests: 5
  • Passed: 5 ✅
  • Failed: 0
  • Skipped: 0
  • Duration: 4.87 min

✅ All smoke tests passed!

All critical flows are working correctly.


This is a non-blocking smoke test. Full regression tests run separately.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile


const LOG_LEVEL_HIERARCHY: LogLevel[] = ["debug", "info", "warn", "error"];

let _globalConsoleLogLevel: ConsoleLogLevel = "debug";
Copy link
Contributor

Choose a reason for hiding this comment

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

global mutable state defaults to "debug" (permissive), logs may appear before ConsoleSink constructor runs

Suggested change
let _globalConsoleLogLevel: ConsoleLogLevel = "debug";
let _globalConsoleLogLevel: ConsoleLogLevel = "silent";
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/globalConsoleLevel.ts
Line: 5:5

Comment:
global mutable state defaults to `"debug"` (permissive), logs may appear before `ConsoleSink` constructor runs

```suggestion
let _globalConsoleLogLevel: ConsoleLogLevel = "silent";
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

16 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +22 to 24
"@crossmint/common-sdk-base": "workspace:*",
"zod": "3.22.4"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Heavy transitive dependencies introduced for a lightweight utility

@crossmint/common-sdk-base declares viem and @solana/web3.js as runtime dependencies (see its package.json). Adding it here pulls those as transitive install-time dependencies for every consumer of @crossmint/client-sdk-window — even for apps that don't use wallets. The logToConsole utility itself only needs two string-literal types and a mutable module-level variable; it has no actual runtime dependency on those heavy packages.

The PR description's human review checklist already flags this concern. Two lower-impact alternatives worth evaluating before merging:

  1. Extract logToConsole into a dedicated micro-package (e.g. @crossmint/logger-utils) with zero production dependencies. Both common-sdk-base and client-sdk-window could depend on it without the bundlesize hit.
  2. Injectable logger pattern — pass a logger callback into EventEmitter/HandshakeParent/HandshakeChild constructors so callers supply their own logToConsole (or a no-op) rather than importing it from common-sdk-base directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/client/window/package.json
Line: 22:24

Comment:
**Heavy transitive dependencies introduced for a lightweight utility**

`@crossmint/common-sdk-base` declares `viem` and `@solana/web3.js` as runtime dependencies (see its `package.json`). Adding it here pulls those as transitive install-time dependencies for every consumer of `@crossmint/client-sdk-window` — even for apps that don't use wallets. The `logToConsole` utility itself only needs two string-literal types and a mutable module-level variable; it has no actual runtime dependency on those heavy packages.

The PR description's human review checklist already flags this concern. Two lower-impact alternatives worth evaluating before merging:

1. **Extract `logToConsole` into a dedicated micro-package** (e.g. `@crossmint/logger-utils`) with zero production dependencies. Both `common-sdk-base` and `client-sdk-window` could depend on it without the bundlesize hit.
2. **Injectable logger pattern** — pass a `logger` callback into `EventEmitter`/`HandshakeParent`/`HandshakeChild` constructors so callers supply their own `logToConsole` (or a no-op) rather than importing it from `common-sdk-base` directly.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 41 to 43
log(...args: unknown[]): void {
if (shouldLogToConsole("debug")) console.log(...args);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

logToConsole.log maps to the "debug" level — may silently suppress expected informational messages

logToConsole.log calls shouldLogToConsole("debug"), so it is suppressed whenever consoleLogLevel is set to any level stricter than "debug" (i.e. "info", "warn", "error", or "silent"). The call sites ("User already logged in", "User not logged in") are informational messages that most developers would expect to appear when consoleLogLevel="info".

If these are intentionally debug-level, using logToConsole.debug (which also maps to console.debug) would be more explicit and self-documenting. If they are intended as info-level, they should use logToConsole.info instead.

Suggested change
log(...args: unknown[]): void {
if (shouldLogToConsole("debug")) console.log(...args);
},
log(...args: unknown[]): void {
if (shouldLogToConsole("info")) console.log(...args);
},

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/globalConsoleLevel.ts
Line: 41:43

Comment:
**`logToConsole.log` maps to the `"debug"` level — may silently suppress expected informational messages**

`logToConsole.log` calls `shouldLogToConsole("debug")`, so it is suppressed whenever `consoleLogLevel` is set to any level stricter than `"debug"` (i.e. `"info"`, `"warn"`, `"error"`, or `"silent"`). The call sites (`"User already logged in"`, `"User not logged in"`) are informational messages that most developers would expect to appear when `consoleLogLevel="info"`.

If these are intentionally debug-level, using `logToConsole.debug` (which also maps to `console.debug`) would be more explicit and self-documenting. If they are intended as info-level, they should use `logToConsole.info` instead.

```suggestion
    log(...args: unknown[]): void {
        if (shouldLogToConsole("info")) console.log(...args);
    },
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@@ -0,0 +1,44 @@
import type { ConsoleLogLevel, LogLevel } from "./types";

const LOG_LEVEL_HIERARCHY: LogLevel[] = ["debug", "info", "warn", "error"];
Copy link
Contributor

Choose a reason for hiding this comment

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

LOG_LEVEL_HIERARCHY duplicated from ConsoleSink.ts

The identical ["debug", "info", "warn", "error"] array is already defined in ConsoleSink.ts:8. Consider extracting it to types.ts or a shared constants file so both files import from a single source of truth.

Suggested change
const LOG_LEVEL_HIERARCHY: LogLevel[] = ["debug", "info", "warn", "error"];
import type { ConsoleLogLevel, LogLevel } from "./types";
import { LOG_LEVEL_HIERARCHY } from "./types";

(Only if LOG_LEVEL_HIERARCHY is moved to types.ts; otherwise, extract it to a shared constants.ts in the same directory.)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/globalConsoleLevel.ts
Line: 3:3

Comment:
**`LOG_LEVEL_HIERARCHY` duplicated from `ConsoleSink.ts`**

The identical `["debug", "info", "warn", "error"]` array is already defined in `ConsoleSink.ts:8`. Consider extracting it to `types.ts` or a shared constants file so both files import from a single source of truth.

```suggestion
import type { ConsoleLogLevel, LogLevel } from "./types";
import { LOG_LEVEL_HIERARCHY } from "./types";
```

(Only if `LOG_LEVEL_HIERARCHY` is moved to `types.ts`; otherwise, extract it to a shared `constants.ts` in the same directory.)

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +18 to +20
constructor(private minLogLevel: ConsoleLogLevel = "debug") {
setGlobalConsoleLogLevel(minLogLevel);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Global level can be overwritten to "debug" by a second ConsoleSink instantiation

setGlobalConsoleLogLevel writes unconditionally to a module-level mutable variable. If a second ConsoleSink is constructed after the first (e.g. if sinkManager.init() is bypassed or called in an integration test), the new instance will override the level set by the first. More critically, if wallets-sdk's initWalletsLogger ever runs before CrossmintProvider mounts (the sinkManager is empty), it will create new ConsoleSink("debug") which resets the global level to "debug" — even if the user later sets consoleLogLevel="silent" on CrossmintProvider. The reverse ordering check noted in the PR description is a real risk; consider adding a guard so the global level can only be promoted (made stricter), not demoted:

export function setGlobalConsoleLogLevel(level: ConsoleLogLevel): void {
    // Only allow raising the minimum level, never lowering it
    if (level === "silent") {
        _globalConsoleLogLevel = "silent";
        return;
    }
    const currentIdx = _globalConsoleLogLevel === "silent"
        ? Infinity
        : LOG_LEVEL_HIERARCHY.indexOf(_globalConsoleLogLevel);
    const newIdx = LOG_LEVEL_HIERARCHY.indexOf(level);
    if (newIdx > currentIdx) {
        _globalConsoleLogLevel = level;
    }
}

This prevents a later, lower-priority sink from accidentally downgrading a previously configured silent level.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/common/base/src/logger/sinks/ConsoleSink.ts
Line: 18:20

Comment:
**Global level can be overwritten to `"debug"` by a second `ConsoleSink` instantiation**

`setGlobalConsoleLogLevel` writes unconditionally to a module-level mutable variable. If a second `ConsoleSink` is constructed after the first (e.g. if `sinkManager.init()` is bypassed or called in an integration test), the new instance will override the level set by the first. More critically, if `wallets-sdk`'s `initWalletsLogger` ever runs before `CrossmintProvider` mounts (the `sinkManager` is empty), it will create `new ConsoleSink("debug")` which resets the global level to `"debug"` — even if the user later sets `consoleLogLevel="silent"` on `CrossmintProvider`. The reverse ordering check noted in the PR description is a real risk; consider adding a guard so the global level can only be promoted (made stricter), not demoted:

```ts
export function setGlobalConsoleLogLevel(level: ConsoleLogLevel): void {
    // Only allow raising the minimum level, never lowering it
    if (level === "silent") {
        _globalConsoleLogLevel = "silent";
        return;
    }
    const currentIdx = _globalConsoleLogLevel === "silent"
        ? Infinity
        : LOG_LEVEL_HIERARCHY.indexOf(_globalConsoleLogLevel);
    const newIdx = LOG_LEVEL_HIERARCHY.indexOf(level);
    if (newIdx > currentIdx) {
        _globalConsoleLogLevel = level;
    }
}
```

This prevents a later, lower-priority sink from accidentally downgrading a previously configured silent level.

How can I resolve this? If you propose a fix, please make it concise.


const LOG_LEVEL_HIERARCHY: LogLevel[] = ["debug", "info", "warn", "error"];

let _globalConsoleLogLevel: ConsoleLogLevel = "debug";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Slow down there, little lizard 🦎 — defaulting to "silent" would mean zero logs for any SDK consumer who doesn't explicitly set consoleLogLevel. That's a terrible default! The whole point of "debug" as default is: log everything unless the developer opts out. The ConsoleSink constructor sets the correct level once CrossmintProvider mounts.

Keeping "debug" as default is intentional and correct.

"dev": "tsup --watch"
},
"dependencies": {
"@crossmint/common-sdk-base": "workspace:*",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our scaly friend does have a point here 🐍 — already flagged this in the PR description's human review checklist. In practice though, client-sdk-window is only consumed by other internal SDK packages (wallets, react-base) which already depend on common-sdk-base, so the transitive dep impact on end-user bundle size is minimal since tree-shaking/bundlers won't pull viem/@solana/web3.js just from importing logToConsole.

That said, the injectable logger or micro-package approach would be cleaner architecturally — deferring to human reviewers on whether to take that on in this PR or a follow-up.

if (shouldLogToConsole("error")) console.error(...args);
},
log(...args: unknown[]): void {
if (shouldLogToConsole("debug")) console.log(...args);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I'll give it to you on this one, little gecko 🦎 — console.log() call sites like "User already logged in" are indeed informational, not debug-level. I'll update logToConsole.log to map to "info" level.

@@ -0,0 +1,44 @@
import type { ConsoleLogLevel, LogLevel } from "./types";

const LOG_LEVEL_HIERARCHY: LogLevel[] = ["debug", "info", "warn", "error"];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair enough, little chameleon 🦎 — extracting LOG_LEVEL_HIERARCHY to a shared constant. Will do.


constructor(private minLogLevel: ConsoleLogLevel = "debug") {}
constructor(private minLogLevel: ConsoleLogLevel = "debug") {
setGlobalConsoleLogLevel(minLogLevel);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Easy there, Komodo dragon 🐉 — the "only promote, never demote" approach you're suggesting would actually break legitimate use cases. If a user dynamically changes consoleLogLevel from "silent" to "debug" (e.g., toggling a debug panel), your guard would prevent it from taking effect.

The real-world initialization order is: CrossmintProvider mounts → creates ConsoleSink with user's consoleLogLevel → sets global level. The wallets-sdk logger is initialized after the provider (since wallet creation happens post-login), so the ordering concern doesn't apply here.

The current "last write wins" is the correct and simplest semantic. The PR description already flags the ordering consideration for human reviewers.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

17 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

18 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

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.

1 participant

Comments