Repository navigation
feat(logger): Publish the logger as @sentry/logger - #1773
Conversation
Move the logger from packages/cli/src/lib/logger.ts into a new packages/cli-logger workspace package, published to npm as @sentry/cli-logger. createLogger() takes the CLI-specific pieces as options: env (the CLI passes getEnv for library mode) and prompt (the CLI keeps its completion-build guard around clackPrompt). formatLogTime moves into the package as the default formatLocalTime. The CLI's logger.ts keeps its exports, so callers and test mocks do not change. Releases go through the Release workflow's new cli-logger product. Fixes #1772 Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3b25814. Configure here.
Address review feedback on the package: - Rename @sentry/cli-logger to @sentry/logger (packages/logger), since nothing in it is CLI-specific. Start at 1.0.0-dev.0 under MIT. - Restructure around structured records: each call becomes a LogRecord (level, type, tag, args, attributes) that fans out to reporters. Ship fancyReporter (consola-style), jsonReporter, logfmtReporter, and sentryReporter, which takes the SDK's `logger` so the package has no Sentry dependency. Add withAttributes() for structured fields. - Move prompts out of the package into the CLI (src/lib/prompt.ts), so the logger has no dependencies at all. - Drop the logger's preReleaseCommand: Craft v3 bumps package.json in the workspace directory through the npm target. - Keep the packed-consumer check as real files under test/package/consumer, run by test/package/check.sh. - Explain in the README why we wrote another logger. The CLI now forwards logs with sentryReporter(Sentry.logger), so its Sentry log attributes change from consola.* to logger.tag/logger.type.
| # No preReleaseCommand: Craft bumps package.json through the npm target. | ||
| packages/logger: | ||
| changelog: | ||
| policy: auto | ||
| releaseBranchPrefix: release/logger | ||
| targets: | ||
| - name: github | ||
| tagPrefix: "logger@" | ||
| includeNames: '/^sentry-logger-\d.*\.tgz$/' | ||
| - name: npm | ||
| id: "@sentry/logger" | ||
| includeNames: '/^sentry-logger-\d.*\.tgz$/' |
There was a problem hiding this comment.
Logger Craft workspace never bumps package.json on release
packages/logger has no preReleaseCommand, so Craft prepare leaves package.json at 1.0.0-dev.0 while tagging the release version; packed/published artifacts stay on the dev version and later bumps recompute the same target.
Evidence
- Release prepare passes an explicit new version into Craft (
release.yml→getsentry/craft@v3), but versioning only lands in the repo ifpreReleaseCommandwrites it. packages/cliandpackages/mcp-serverboth bump viapreReleaseCommand(npm version "$CRAFT_NEW_VERSION"/ bump script); mcp even bumpspackages/mcp-serveritself that way.- The new logger workspace comment claims the npm target bumps
package.json, but that target only publishesincludeNames: '/^sentry-logger-\d.*\.tgz$/'artifacts built earlier bypnpm packin merge-jobs from whatever version is already inpackages/logger/package.json. - Starting version is
1.0.0-dev.0, so without a prepare-time bump the release branch, tarball, and npm publish remain on the dev version while the GitHub tag can still belogger@<new>.
Identified by Warden · code-review · 924-ZLC
There was a problem hiding this comment.
Not a bug. Without a preReleaseCommand, Craft v3's runPreReleaseCommand (src/commands/prepare.ts) runs automatic version bumps from the workspace's targets when minVersion >= 2.21.0 (here 2.33.0). The npm target's bumpVersion runs npm version <new> --no-git-tag-version in the workspace directory (getReleaseUnitDir(), getsentry/craft#936).
I checked it with Craft 3.5.0 (the v3 tag), using this .craft.yml and packages/logger/package.json. With workspace packages/logger selected, the resolved config has no preReleaseCommand, targets [github, npm], and root dir packages/logger. runPreReleaseCommand returned true, and package.json went from 1.0.0-dev.0 to 1.0.0.
The bump is committed to release/logger/<version>. That push triggers the post-merge pnpm pack, so the tarball gets the release version.
BYK
left a comment
There was a problem hiding this comment.
Great work. Let's also measure the performance/overhead of the library and make sure there's close to zero overhead.
- Drop the separate Logger Package workflow. The package declares
`sentryCi.e2e: test:package`, so the Test workflow's project matrix
packs it and runs the clean consumer check whenever the logger or a
dependent changes.
- Use Craft's {{workspaceName}} and {{packageSimpleName}} placeholders
for the logger workspace's branch prefix, tag prefix, and tarball
names. Craft 3.5 resolves them to the same values as before.
- Follow logfmt's key rules: only spaces, `=`, `"`, and control
characters are replaced, so dotted keys like `http.status_code` pass
through unchanged.
Measure the logger against the cheapest code that does the same job, and cut the costs the benchmark found. - Add `pnpm --filter @sentry/logger run bench`, which times each case in its own Node process, and record its results in the README. - Format each record's ISO timestamp once per second instead of calling `toISOString()` per line. - Build the structured fields as an object, so the JSON reporter stringifies it directly; keep a `__proto__` attribute as a field. - Measure visible width with a loop instead of an ANSI regex replace, and skip the markup regexes when a line has no delimiters. - Test logfmt keys before replacing characters in them. Output is byte-identical to the previous build across about 24,600 formatted records.
|
@BYK I measured it and trimmed the hot paths in
The fixes:
Output from the new build is byte-identical to the previous one across about 24,600 records: every reporter, layout, env combination, and edge-case date. I added tests for the timestamp cache (millisecond padding, second boundaries, negative epochs, year 20000) and for a |
BYK
left a comment
There was a problem hiding this comment.
Keep addressing my comments but I think this is mostly good to go now.
Attribute values can now be objects. The logfmt and Sentry reporters expand them into dotted keys (`http.status_code=404`); the JSON reporter keeps them nested. Expansion stops five levels down and writes `[Object]` there, which caps the cost and stops reference cycles. Flat attributes cost the same as before: a nested attribute adds about 200 ns to a logfmt line, and the Sentry reporter got slightly faster by dropping `Object.entries`. The benchmark gains a nested-attribute case.
| releaseBranchPrefix: "release/{{workspaceName}}" | ||
| targets: | ||
| - name: github | ||
| tagPrefix: "{{workspaceName}}@" | ||
| includeNames: '/^{{packageSimpleName}}-\d.*\.tgz$/' | ||
| - name: npm | ||
| includeNames: '/^{{packageSimpleName}}-\d.*\.tgz$/' |
There was a problem hiding this comment.
Logger Craft config leaves release placeholders unresolved
The logger workspace still uses literal {{workspaceName}} / {{packageSimpleName}} values, so release branches, tags, and artifact filters will not match release/logger/** or the real sentry-logger-*.tgz pack name and the release cannot publish.
Evidence
- Sibling workspaces use concrete values (
release/cli,release/mcp,cli@,/^sentry-mcp-server-\d.*\.tgz$/), while logger keeps mustache-style placeholders inreleaseBranchPrefix,tagPrefix, and bothincludeNames. .github/workflows/merge-jobs.ymlonly builds release artifacts forrelease/logger/**, which does not match a branch prefix ofrelease/{{workspaceName}}orrelease/packages/logger.packages/logger/test/package/check.shpacks@sentry/loggerassentry-logger-*.tgz, so/^{{packageSimpleName}}-\d.*\.tgz$/(or even/^logger-\d.*\.tgz$/) will not select the uploaded artifact.
Identified by Warden · code-review · 4C3-EPT
| } else if (error.cause) { | ||
| cause = `${stack ? `\n\n${" ".repeat(depth + 1)}` : " "}[cause]: ${formatWithOptions(INSPECT_OPTIONS, error.cause)}`; | ||
| } |
There was a problem hiding this comment.
Falsy Error.cause values are silently dropped
else if (error.cause) skips valid falsy causes (0, "", false, null), so they never appear in the formatted message despite the function's contract to render causes.
Evidence
formatErroronly appends a non-ErrorLike cause whenerror.causeis truthy (else if (error.cause)).Erroraccepts any cause, including0,"",false, andnull; Node's ownconsole/util.inspectprint those as[cause]: ….- Missing cause is
undefined, so the correct presence check iserror.cause !== undefined(or'cause' in error), not truthiness. - Result:
formatMessage([new Error("x", { cause: 0 })], { stack: false })yields"x"instead of"x [cause]: 0".
Identified by Warden · code-review · 79U-429
| export function formatMessage( | ||
| args: readonly unknown[], | ||
| { stack }: { stack: boolean }, | ||
| ): string { | ||
| return formatWithOptions( | ||
| INSPECT_OPTIONS, | ||
| ...args.map((arg) => (isErrorLike(arg) ? formatError(arg, 0, stack) : arg)), | ||
| ); |
There was a problem hiding this comment.
Error-as-first-arg is treated as a util.format string
Pre-stringifying Errors before formatWithOptions makes an Error in args[0] act as a printf format string, so %s/%d in the error message consume later arguments and corrupt the log line.
Evidence
- File contract: format "the way
console.logwould" / with "util.formatsemantics". formatMessagemaps ErrorLike args throughformatErrorto plain strings, then spreads them intoformatWithOptions.util.format(error, extra)does not treatErroras a format string;util.format("failed %s\n…", extra)does.- Trigger:
formatMessage([new Error("failed %s"), "payload"], { stack: false })returns"failed payload"instead of keeping%sand appendingpayload.
Identified by Warden · code-review · PKW-NHK
Brings in #1773 (@sentry/logger) and #1759 (flag completion kinds). Conflicts: - .craft.yml, merge-jobs.yml, release.yml: keep main's logger release workspace, branch, packaging, and product; keep the removal of the separate mcp release (the CLI release publishes @sentry/mcp-server). - packages/cli/package.json: depend on @sentry/logger and @sentry/mcp-core, not the retired @sentry/mcp-server workspace. - pnpm-lock.yaml: regenerated with pnpm install. Follow-ups for the merged tree: - `sentry mcp` string flags parse with parseText, as #1759's lint rule requires. - completion-target.ts now imports commands/local/loopback.ts, so allow that leaf module in the mcp-server bundle's CLI-only input check. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
Checks for a single package should run from the `test.yml` per-project matrix, set up through `package.json#sentryCi`. Nothing pointed contributors or agents there, so #1773 first added a standalone `logger-package.yml`, copying the existing `mcp-server-package.yml`. - **Moves the `@sentry/mcp-server` clean-consumer typecheck into the matrix.** It's now `sentryCi.e2e: "test:package"`, laid out like `@sentry/logger`'s check: `test/package/check.sh` plus a `consumer/` fixture with the same `index.ts` the workflow used to write inline. `mcp-server-package.yml` is removed. The matrix already selects the package when it or one of its workspace dependencies (`mcp-core`, `mcp-server-mocks`, the tsconfig package) changes, and for root changes and pushes to `main`. The check now also sits behind the required `test` job; before, "Clean Consumer Typecheck" was not a required check. One small difference: matrix E2E steps run on Node 24, while the old workflow ran on Node 22. - **Documents `sentryCi`.** `docs/operations/github-actions.md` gets a "Package CI checks" section: the default script for each field, the override fields, an example, and when a separate workflow is still justified. `AGENTS.md` and `docs/contributing/quality-checks.md` add a short rule that links to it. - **Adds a guard.** `scripts/workflow-policy.test.mjs`, run by `test:ci-projects` in the quality job, fails when a workflow triggers on `pull_request`, `pull_request_target`, or `merge_group` and isn't on its list with a reason. The failure message points to `sentryCi`. The existing PR workflows are on the list: eval, install scripts, Local multi-OS E2E, PR risk, smoke tests, and token cost. Fixes #1777 <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:local%3Aweb%3Ac828b56c95c28d1bcb6f29fa --> -- [View Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3Ac828b56c95c28d1bcb6f29fa/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
This fixes the Warden findings that were posted on #1773 after it merged. - **Falsy causes:** `formatError` showed a cause only when it was truthy, so `new Error("x", { cause: 0 })` printed just `x`. It now shows the cause whenever the error has one (`0`, `""`, `false`, `null`, `undefined`), as `util.inspect` does. - **An error as the first argument:** `formatMessage` turned errors into strings before calling `util.format`, so an error passed first became the format string. `logger.info(new Error("Failed for %s"), "extra")` printed `Failed for extra`. The `%s` now stays literal and the remaining arguments are added after it, as `console.log` does. - **Craft `minVersion`:** raised from 2.33.0 to 3.5.0. The logger workspace uses `{{workspaceName}}` and `{{packageSimpleName}}`, and Craft only resolves those from 3.5.0 onward. The release workflow's `getsentry/craft@v3` already installs 3.5.0. Warden said the placeholders are left unresolved, which is wrong for the release workflow, but an older local Craft would have left them as literal text without any error. This raises the minimum for every workspace in this repository. Tests cover each falsy cause and an error message containing `%s`. For other output, the build is byte-identical to `main` across about 24,600 formatted records. <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:local%3Aweb%3A052a28796a4c37b4b8b5f916 --> -- [View Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A052a28796a4c37b4b8b5f916/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

Fixes #1772.
Moves the logger from #1771 out of the CLI into
packages/logger, published to npm as@sentry/logger: a small structured logger for Node.js and Bun with no dependencies.Design
LogRecord(date,args,type,level,tag, plusattributes) and fans out to the logger's reporters. Records keep consola's shape, so consola-style reporters still read them.createLogger({ level, reporters })returns a root logger.withTag()andwithAttributes()return scoped loggers that share the root's level and reporter list.fancyReporter(consola's fancy layout, the default),jsonReporter(NDJSON),logfmtReporter, andsentryReporter(Sentry.logger). The Sentry reporter takes the SDK'sloggeras an argument and is typed structurally, so the package has no@sentry/*dependency, optional or otherwise.node:utilandnode:path. About 5 KB minified, 2.5 KB gzipped, with every reporter.CLI changes
packages/cli/src/lib/prompt.ts(same@clack/promptslazy import andSENTRY_CLI_COMPLETION_BUILDguard), and call sites useprompt()instead oflog.prompt(). Tests mock the prompt module instead of a fake logger'sprompt.fancyReporter({ env: getEnv })and addssentryReporter(Sentry.logger)afterSentry.init().logger.tag/logger.typeandsentry.origin: auto.log.sentry_loggerinstead ofSentry.createConsolaReporter()'sconsola.*attributes. I found nothing in this repo that queries the old names. Saved queries or dashboards that do will need updating.Packaging and release
exportspoint atsrc/*.ts, so the CLI bundle, vitest, and tsc need no build step.publishConfig.exportspoints the tarball at the tsdown ESM/CJSdist/.loggercraft workspace (tag prefixlogger@, release branchrelease/logger/*) and aloggeroption in the Release workflow. It has nopreReleaseCommand: Craft v3 bumpspackage.jsonin the workspace directory through the npm target. Post-merge jobs pack the tarball into the commit's artifact.Logger Packageworkflow runstest/package/check.sh: it packs the package, installs it into the consumer project intest/package/consumer/, then typechecks and runs it as ESM and as CommonJS.1.0.0-dev.0, so the first Release run publishes1.0.0whichever bump it uses.package.jsonin a script, because Craft applies bump types to the latest tag (0.0.0for a new workspace). Filed Bump types ignore the workspace's manifest version craft#946.--
View Session in Sentry