metro-object-logs
v0.1.0
Published
Real values from your React Native logs in the Metro terminal instead of [Object], and the complete payload in a file your coding agent can read.
Maintainers
Readme
metro-object-logs
Real values from your React Native logs in the Metro terminal instead of [Object], and the complete payload in a file your coding agent can read.
Metro truncates logged objects to {"a": [Object]} and LogBox flattens the rest. This library keeps the terminal readable and keeps the whole object, in two channels:
# terminal (Metro)
LOG order snapshot {id: "ord_8213", customer: {id: 42, name: "Ada Lovelace", email: "[email protected]"}, items: [{3: sku, qty, price}, {3: sku, qty, price}], totals: {subtotal: 228, tax: 18.24, shipping: 0, grand: 246.24}} #log_7 ↳log_7
# metro-objects.log — one JSON line per call, nothing elided
{"type":"log","id":"log_7","session":"k3f1","method":"log","ts":1767330001000,"args":["order snapshot",{"id":"ord_8213","customer":{"id":42,"name":"Ad…The #log_7 tag is the join key. You (or your agent) run npx metro-object-logs get log_7 and get the entire object back.
Installation
npm install metro-object-logs
# or
yarn add metro-object-logsA regular dependency, not a dev one: your app code imports it, so Metro has to resolve it when bundling a release too. It compiles down to a no-op there.
Usage
1. Set the project up — one command, run once:
npx metro-object-logs init
# or
yarn metro-object-logs initIt shows you a diff and asks before writing anything:
--- index.js
+ import { installMetroObjectLogs } from 'metro-object-logs';
import { AppRegistry } from 'react-native';
import App from './src/App';
+
+ installMetroObjectLogs();
AppRegistry.registerComponent(appName, () => App);
--- package.json
+ "start:logs": "metro-object-logs start"
--- .gitignore
+ # metro-object-logs output
+ metro.log
+ metro-objects.log
+ metro-objects.log.prev
Apply 3 changes? [y/N]--dry-run shows the diff and exits, --yes skips the prompt, --entry <path> overrides which file gets the import. Running it again is a no-op. Without a TTY it refuses unless you pass --yes, so it can't quietly rewrite files in CI.
It is a deliberate command rather than a postinstall hook: nothing edits your files as a side effect of npm install. To do it by hand instead, add this to your entry file — as early as possible — and a metro-object-logs start script:
import { installMetroObjectLogs } from 'metro-object-logs';
installMetroObjectLogs();Safe to leave in permanently. It is a no-op unless __DEV__ is true and the CLI sidecar is running, so release builds and a plain react-native start are untouched. Where __DEV__ isn't defined at all — a non-Metro bundler, a plain Node script — it stays off; pass enabled: true to force it.
2. Start Metro through the CLI instead of directly:
npm run start:logs # the script init adds
# or
yarn start:logsWithout running init, call the binary directly:
npx metro-object-logs start
# or
yarn metro-object-logs startThat wraps your normal start command (auto-detecting Expo vs. the React Native CLI), keeps Metro's interactive keys working, and adds:
- a capture sidecar on
127.0.0.1:8088, which the app discovers automatically metro-objects.log— one JSON line per logged call, full depthmetro.log— everything printed to the terminal, mirrored to a file
Both files land in the current directory; init already added them to .gitignore.
3. Read a payload back:
npx metro-object-logs get log_7 # the full object behind #log_7
npx metro-object-logs tail -n 20 # the 20 most recent entries
npx metro-object-logs clear # empty both log filesWith Yarn, swap npx for yarn:
yarn metro-object-logs get log_7react-native start still works exactly as before — no sidecar, no patch, no log files.
Chrome DevTools
The Metro terminal and Chrome DevTools are fed by the same CDP
Runtime.consoleAPICalled event, so they receive identical arguments — what
differs is the rendering. The terminal only ever has CDP's shallow preview, which
is why plain React Native prints [Object] for anything nested; DevTools holds
an objectId and fetches the rest on demand.
So the original objects are passed along after the preview, and DevTools shows
them as real, expandable values rather than text. In the terminal they collapse
to a ↳log_7 marker instead of printing twice, via a util.inspect hook that
Metro's reporter honours.
If you never attach a debugger and want the line to end at the #id tag:
installMetroObjectLogs({ keepObjectsForDebugger: false });Using this with an AI coding agent
This is what the library is for. Agents can't read your Metro terminal, and pasting [Object] at them doesn't help. Point yours at the log file instead — drop this in your CLAUDE.md, AGENTS.md, or equivalent:
## Reading React Native logs
The dev server runs via `npx metro-object-logs start`, which writes two files:
- `metro.log` — the Metro terminal output. Objects appear as compact previews
ending in a `#<id>` tag, e.g. `{id: 7, user: {id: 42, name: "Ada"}} #log_12`.
- `metro-objects.log` — JSONL, one line per logged call, with the complete
object graph. Look up a tag with `npx metro-object-logs get log_12`
(or `yarn metro-object-logs get log_12`).
To investigate a log line: read `metro.log` to find the call, then
`metro-object-logs get <id>` for the full payload. Prefer `get` over reading
`metro-objects.log` directly — lines can be very long.
Each line is `{type, id, session, method, ts, args}`. `session` changes on every
app reload, so ids repeat; the newest match is the current run.What survives the round trip
Everything JSON.stringify would silently drop or mangle is turned into something readable rather than lost:
| Value | Logged as |
| --- | --- |
| Error | {name, message, stack} plus custom props like code |
| Map / Set | {"Map(2)": {…}} / {"Set(2)": […]} |
| Date | ISO string |
| RegExp | /ab+c/gi |
| BigInt | "10n" |
| function / symbol | "[Function fetchUser]" / "Symbol(tag)" |
| Infinity / NaN | "Infinity" / "NaN" |
| typed array | "[Uint8Array(3)]" |
| circular reference | "[Circular]" |
Cycles are detected against the current ancestor chain, so an object referenced from two different branches is serialised twice rather than mislabelled [Circular].
Payloads are capped so one enormous log cannot stall the app — depth 12, 200 keys per object, 500 items per collection, 10,000 characters per string. Anything clipped is marked ([+12 keys], [MaxDepth Object]). Raise the caps if you need to:
installMetroObjectLogs({
limits: { maxDepth: 20, maxStringLength: 100_000 },
});The terminal preview
Values are shown two levels deep, then summarised as {<count>: key, key}:
{id: 42, email: "[email protected]", profile: {name: "Ada Lovelace", locale: "en-GB", verified: true}, settings: {theme: "dark", notifications: true, betaFeatures: false}} #log_7An array spends one of those levels on itself, so its elements still show values rather than only key names — otherwise every row renders identically and you cannot tell them apart:
[{id: 42, email: "[email protected]", profile: {3: name, locale, verified}}, {id: 43, email: "[email protected]", profile: {3: name, locale, verified}}] #log_8Top-level arrays are capped at 5 elements, because depth costs roughly 3× the
line width on arrays — a 100-row log would otherwise run to ~15,000 characters.
With the cap it stays around 800 no matter how long the array is, and the full
array is still in metro-objects.log.
installMetroObjectLogs({
preview: { depth: 1, arrayLimit: 20 }, // shallower, but more rows
});Options
installMetroObjectLogs({
enabled: __DEV__, // force on/off; defaults to __DEV__
host: undefined, // defaults to the host Metro served the bundle from
port: 8088, // must match --sidecar-port
methods: ['log', 'info', 'debug', 'error'],
probeTimeoutMs: 800, // how long to wait for the sidecar
limits: {}, // see above
preview: { depth: 2, arrayLimit: 5 }, // terminal preview shape
keepObjectsForDebugger: true, // trail originals so DevTools can expand them
});Returns an uninstall function that restores the original console methods.
console.warn is deliberately never patched — LogBox parses it, and rewriting the arguments breaks its yellow-box formatting. Calls with no object argument are passed through untouched. Repeated installs are ignored, so Fast Refresh cannot double-wrap console.
If the sidecar dies mid-session, the patch removes itself after five failed posts and warns once, so you are never left logging into a void.
CLI
metro-object-logs init [options]
--entry <path> entry file to patch (default: auto-detected)
--dry-run show the diff and exit without writing
-y, --yes skip the confirmation prompt
metro-object-logs start [options] [-- <metro args>]
--sidecar-port <n> port for the capture sidecar (default 8088)
--sidecar-host <ip> bind address (default 127.0.0.1)
--command <cmd> start command to wrap (default: auto-detected)
--out-dir <dir> where to write the log files (default: cwd)
--max-log-mb <n> rotate metro-objects.log past this size (default 64)
--append keep existing log contents instead of truncating
--no-client-logs don't pass --client-logs to react-native start
--no-metro-log skip PTY capture of the terminal output
metro-object-logs get <id> [--all]
metro-object-logs tail [-n <count>]
metro-object-logs clearUnrecognised flags are forwarded to the start command, as is everything after a literal --:
npx metro-object-logs start --port 8082 --reset-cache
npx metro-object-logs start --command "npx expo start" -- --tunnel
yarn metro-object-logs start --port 8082 --reset-cache
yarn metro-object-logs start --command "yarn expo start" -- --tunnelPhysical devices
Emulators and simulators work with no configuration: the app derives the sidecar host from the URL Metro served the bundle from.
A physical device is on your LAN, so the sidecar has to be reachable there:
npx metro-object-logs start --sidecar-host 0.0.0.0
# or
yarn metro-object-logs start --sidecar-host 0.0.0.0Only do that on a network you trust. /log is unauthenticated by design, so anything on the LAN can append lines to metro-objects.log — a file you may be feeding to an AI agent. The default bind is 127.0.0.1 for that reason.
Platform support
The terminal mirror (metro.log) uses POSIX script to allocate a PTY, so Metro's r/d/j keys keep working. That works on macOS and Linux. On Windows, metro-object-logs start warns once and runs without metro.log; full-object capture into metro-objects.log still works everywhere.
Contributing
License
MIT
