
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
react-native-nitro-logger
Advanced tools
Structured, privacy-tiered logging for React Native — TS core with a native Nitro file/os_log sink
Structured, privacy-tiered logging for React Native. A TypeScript core with native file and system-console sinks built on Nitro Modules.
npm install react-native-nitro-logger react-native-nitro-modules
react-native-nitro-modules is a required peer — this library is built on
Nitro Modules.
import {
Log,
ConsoleDestination,
FileDestination,
createFileSink,
} from 'react-native-nitro-logger';
Log.addDestination(new ConsoleDestination());
Log.addDestination(new FileDestination(createFileSink()));
Log.info('app started');
Log.warning('retrying upload', { attempt: 2, statusCode: 503 });
A scoped logger tags every line with a correlation ID, so one request's lines can be picked out of a busy file. Omit the correlation argument and one is generated for you — which is also what the lint rules want, since a correlation ID must never be derived from an identifier you already have:
const scope = Log.scoped(undefined, 'checkout', { orderKind: 'subscription' });
scope.info('payment authorised');
scope.error('capture failed', { statusCode: 502 });
verbose · debug · info · warning · error · todo
Log.minimumLevel('info'); // globally
Log.subsystem('networking', 'debug'); // and per subsystem
The default profile renders metadata values and is fine for open-source and general application use. Apps handling regulated data should switch to the strict one in their entry point, before anything logs:
import { Log, pub, priv } from 'react-native-nitro-logger';
Log.privacyDefault('private');
Log.metadataKeyCatalog(['requestId', 'statusCode', 'durationMs']);
function onRequestFinished(id: string) {
Log.info('request finished', {
requestId: pub(id), // rendered
statusCode: 200, // redacted — unwrapped, and the default is private
});
}
In a release build (__DEV__ false):
'public' (default) | 'private' | |
|---|---|---|
| bare value | rendered | <private> |
pub(v) | rendered | rendered |
priv(v) | <private> | <private> |
A debug build renders private payloads in the clear, to every destination including the file — so the fail-closed behaviour above is a property of the builds that ship, not of every build. A build where reveal is possible is a build for synthetic data only.
privacyDefault is first-set-wins and tighten-only, so a dependency cannot
loosen your setting.
Message text, metadata keys, subsystems and correlation IDs are public by contract — they are never redacted at runtime. The bundled ESLint plugin constrains them at build time instead, but only once you enable it: installing this package ships the rules, it does not apply them.
// eslint.config.mjs
import nitroLogger from 'react-native-nitro-logger/eslint-plugin';
export default [nitroLogger.configs.strictTypeScript];
strictTypeScript covers .ts, .tsx and JavaScript, so it is the only
entry a React Native app needs — pick one config, not both. It needs
@typescript-eslint/parser (an optional peer, no version constraint), which
most RN apps already have via @react-native/eslint-config.
Used on their own, configs.strict and configs.recommended lint
JavaScript only. A flat config with no files key applies to ESLint's
default set — .js, .mjs, .cjs — so on their own they never bring .ts
or .tsx into the linted set, and eslint . exits 0 without a word. If some
other entry in your config already matches TypeScript and supplies a parser,
these rules do run there as well — that is incidental composition, not
something to rely on, and it is exactly why this repository's own CI stayed
green while the published config was inert. It holds only while that other
entry keeps selecting TypeScript and supplying a compatible parser. Reach for
the bare configs if your sources are JavaScript, or if you have Flow-annotated
.js that the TypeScript parser would reject, in which case compose strict
with your own parser.
Read docs/PRIVACY.md before using this in an app that handles regulated data. It covers what the contract does and does not promise, the approved-key catalog, and the compliance boundary — in short, a build where reveal is possible is a build for synthetic data only.
const file = new FileDestination(createFileSink(), {
rotation: {
maxFileSizeBytes: 10 * 1024 * 1024,
maxArchivedFilesCount: 5,
compressArchives: true,
maxTotalLogBytes: 50 * 1024 * 1024,
},
});
Log.addDestination(file);
Logs live in app-private storage — noBackupFilesDir on Android, Library/Logs
on iOS. Owner-only modes are applied to every artifact; where the platform
refuses, the sink reports a protection degradation and keeps logging rather
than failing shut.
file.getLogFilePaths(); // for a consent-gated support upload
const outcome = file.purge(5000); // the compliance purge
if (!outcome.durable) {
// Something survived, or the deadline blew. The destination stays fenced
// until an explicit retry, so nothing is written into a pending deletion.
}
purge is synchronous and deadline-bounded. It reports durable (every
pre-purge artifact is gone) separately from rebound (the destination is
writable again), because a complete deletion can still be followed by a failed
reopen, and a caller that resumes on durable alone would write into a
destination with nowhere to put anything. A purge that rebinds opens a fresh,
empty active file — durable is a statement about the data that was there, not
a promise that the directory is left empty.
It clears the file sink's artifacts and nothing else. Anything already
handed to os_log or logcat is outside this library's reach.
NativeConsoleDestination writes into the platform log stream — os_log on iOS,
logcat on Android — so your JavaScript lines interleave with native ones in
Console.app, Xcode, and adb logcat.
import {
Log,
NativeConsoleDestination,
createNativeConsoleSink,
} from 'react-native-nitro-logger';
Log.addDestination(
new NativeConsoleDestination(createNativeConsoleSink(), {
subsystem: 'com.example.app',
category: 'network',
})
);
Unlike ConsoleDestination, this one batches: a drain crosses the bridge once
with two parallel primitive arrays rather than once per entry.
It is best-effort by design, and that is the important thing to know about
it. There is no backpressure and no loss accounting against a disk, because
os_log accepts what it is given and never blocks. What it does account for is
its own buffer — dropped() returns the entries lost at the ceiling or to a
sink that threw, because a diagnostic channel that quietly loses records is
worse than one that admits it. After three consecutive native failures the
destination reports isEnabled === false and the logger stops formatting for
it, so a dead sink stops costing render work as well as bridge calls.
The durable copy is FileDestination's. Nothing here is a system of record,
which is what makes best-effort the right posture.
| Option | Default | Notes |
|---|---|---|
subsystem | the bundle's, chosen natively | Reverse-DNS, as os_log expects. Empty is legal and produces a logger nobody can find. |
category | 'log' | Becomes the logcat tag on Android. |
formatter | DefaultFormatter | os_log wants a line, not a JSON record. |
label | 'native-console' | Registration key for removeDestination. |
minimumLevel | inherited | Per-destination floor. |
batchSize | 64 | Entries per bridge crossing. |
flushIntervalMs | 100 | Idle coalescing window. |
maxPendingEntries | 1000 | Buffer ceiling; oldest survive, newest are dropped. |
Long lines are split rather than silently cut off — the two platforms have genuinely different limits, and docs/PARITY.md records both the chunk sizes and why the split boundary differs between them.
import { installErrorHandler, flushOnBackground } from 'react-native-nitro-logger';
const uninstallHandler = installErrorHandler();
const uninstallFlush = flushOnBackground();
The error handler logs uncaught errors with the message dropped outside dev, the class name reduced to a built-in or a fixed token, and stack frames reduced to a position in a file whose name was already known. It flushes on fatal errors, then chains to whatever handler was installed before it. Both functions return idempotent uninstall handles.
JsonLinesFormatter is the default for files — one JSON object per line, with
a framing guarantee the native crash-tail recovery depends on. It is asserted
byte-identical to SwiftLogger's JSONLogFormatter over a
generated corpus, against the specific revision
docs/PARITY.md pins.
{"timestamp":"2026-07-28T10:15:00.123Z","level":"INFO","message":"app started"}
DefaultFormatter is the human-readable one used by the console destination.
Custom formatters implement LogFormatter; declare framing: 'line' to opt
into crash-tail trimming.
| Supported | Verified | |
|---|---|---|
| iOS | React Native ≥ 0.78 | CI builds, launches and exercises a pristine 0.78 consumer app |
| Android | React Native at the example's version; ≥ 0.78 experimental. minSdk 24 | CI runs the writer's unit suite and builds the example; no minimum-version consumer job exists yet |
New Architecture only. The compatibility claims are split per platform on purpose — see docs/PARITY.md for what backs each one, and for where the two native writers genuinely differ.
Absolute links, because these files are not in the npm tarball — they are
repository documents, and a relative link to them dies for anyone reading this
README inside node_modules.
MIT
FAQs
Structured, privacy-tiered logging for React Native — TS core with a native Nitro file/os_log sink
The npm package react-native-nitro-logger receives a total of 2 weekly downloads. As such, react-native-nitro-logger popularity was classified as not popular.
We found that react-native-nitro-logger demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.