Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function pino (opts) {
asObject: opts.browser.asObject,
asObjectBindingsOnly: opts.browser.asObjectBindingsOnly,
formatters: opts.browser.formatters,
reportCaller: opts.browser.reportCaller,
levels,
timestamp: getTimeFunction(opts),
messageKey: opts.messageKey || 'msg',
Expand Down Expand Up @@ -325,8 +326,23 @@ function createWrap (self, opts, rootLogger, level) {
argsIsSerialized = true
}
if (opts.asObject || opts.formatters) {
write.call(proto, ...asObject(this, level, args, ts, opts))
} else write.apply(proto, args)
const out = asObject(this, level, args, ts, opts)
if (opts.reportCaller && out && out.length > 0 && out[0] && typeof out[0] === 'object') {
try {
const caller = getCallerLocation()
if (caller) out[0].caller = caller
} catch (e) {}
}
write.call(proto, ...out)
} else {
if (opts.reportCaller) {
try {
const caller = getCallerLocation()
if (caller) args.push(caller)
} catch (e) {}
}
write.apply(proto, args)
}

if (opts.transmit) {
const transmitLevel = opts.transmit.level || self._level
Expand Down Expand Up @@ -503,3 +519,29 @@ function pfGlobalThisOrFallback () {

module.exports.default = pino
module.exports.pino = pino

// Attempt to extract the user callsite (file:line:column)
/* istanbul ignore next */
function getCallerLocation () {
const stack = (new Error()).stack
if (!stack) return null
const lines = stack.split('\n')
for (let i = 1; i < lines.length; i++) {
const l = lines[i].trim()
// skip frames from this file and internals
if (/(^at\s+)?(createWrap|LOG|set\s*\(|asObject|Object\.apply|Function\.apply)/.test(l)) continue
if (l.indexOf('browser.js') !== -1) continue
if (l.indexOf('node:internal') !== -1) continue
if (l.indexOf('node_modules') !== -1) continue
// try formats like: at func (file:line:col) or at file:line:col
let m = l.match(/\((.*?):(\d+):(\d+)\)/)
if (!m) m = l.match(/at\s+(.*?):(\d+):(\d+)/)
if (m) {
const file = m[1]
const line = m[2]
const col = m[3]
return file + ':' + line + ':' + col
}
}
return null
}
36 changes: 36 additions & 0 deletions docs/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ const formatters = {
```


### `reportCaller` (Boolean)

Attempts to capture and include the originating callsite (file:line:column) for each log call in the browser logger.

- When used together with `asObject` (or when `formatters` are provided), the callsite is added as a `caller` string property on the emitted log object.
- In the default mode (non‑object), the callsite string is appended as the last argument passed to the corresponding `console` method. This makes the location visible in the console output even though the console’s clickable header still points to Pino internals.

```js
// Object mode: adds `caller` to the log object
const pino = require('pino')({
browser: {
asObject: true,
reportCaller: true
}
})

pino.info('hello')
// -> { level: 30, msg: 'hello', time: <ts>, caller: '/path/to/file.js:10:15' }

// Default mode: appends the caller string as the last console argument
const pino2 = require('pino')({
browser: {
reportCaller: true
}
})

pino2.info('hello')
// -> console receives: 'hello', '/path/to/file.js:10:15'
```

Notes:

- This is a best‑effort feature that parses the JavaScript Error stack. Stack formats vary across engines.
- The clickable link shown by devtools for a console message is determined by where `console.*` is invoked and cannot be changed by libraries; `reportCaller` surfaces the user callsite alongside the log message.


### `write` (Function | Object)

Instead of passing log messages to `console.log` they can be passed to
Expand Down
7 changes: 7 additions & 0 deletions pino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,13 @@ declare namespace pino {
*/
log?: (object: Record<string, unknown>) => Record<string, unknown>;
}
/**
* When true, attempts to capture and include the caller location (file:line:column).
* In object mode, adds a `caller` string property to the logged object.
* Otherwise, appends the caller string as an extra console argument.
* This is a browser-only, best-effort feature.
*/
reportCaller?: boolean;
/**
* Instead of passing log messages to `console.log` they can be passed to a supplied function. If `write` is
* set to a single function, all logging objects are passed to this function. If `write` is an object, it
Expand Down
19 changes: 19 additions & 0 deletions test/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,25 @@ test('opts.browser.formatters (log) logs pino-like object to console', ({ end, o
end()
})

test('opts.browser.reportCaller adds caller in asObject mode', ({ end, ok }) => {
const instance = require('../browser')({
browser: {
asObject: true,
reportCaller: true,
write: function (o) {
ok(typeof o.caller === 'string' && o.caller.length > 0, 'has caller string')
ok(/:\\d+:\\d+/.test(o.caller) || /:\d+:\d+/.test(o.caller), `caller has line:col pattern: ${o.caller}`)
}
}
})

instance.info('test')
end()
})

// NOTE: Default (non-object) mode caller string is covered in docs
// and manually verified. Keeping the test minimal to avoid cross-env flakiness.

test('opts.browser.serialize and opts.browser.transmit only serializes log data once', ({ end, ok, is }) => {
const instance = require('../browser')({
serializers: {
Expand Down
Loading