npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-logs

A 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 init

It 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:logs

Without running init, call the binary directly:

npx metro-object-logs start
# or
yarn metro-object-logs start

That 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 depth
  • metro.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 files

With Yarn, swap npx for yarn:

yarn metro-object-logs get log_7

react-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_7

An 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_8

Top-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 clear

Unrecognised 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" -- --tunnel

Physical 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.0

Only 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