Skip to main content

Full Integration for Node.js Logs

About Shipbook​

ShipBook gives you the power to remotely gather, search and analyze your user logs and exceptions in the cloud, on a per-user & session basis.


Requirements​

Node.js version 18.0.0 and up.


Installation​

ShipBookSDK is available through: NPM

npm install @shipbook/node

Integrating ShipBook into your code​

To initialize, add the following:

  import { Shipbook } from '@shipbook/node';

await Shipbook.start("YOUR_APP_ID", "YOUR_APP_KEY");

You can optionally provide the app version:

  await Shipbook.start("YOUR_APP_ID", "YOUR_APP_KEY", "1.0.0");

To have a log on each module you need to create a logger:

  import { Shipbook } from '@shipbook/node';

const log = Shipbook.getLogger("MODULE_NAME");

The usage of the log:

  log.e("the log message"); // Error log
log.w("the log message"); // Warning log
log.i("the log message"); // Info log
log.d("the log message"); // Debug log
log.v("the log message"); // Verbose log

Enable Shipbook debug logging​

If your logs weren't uploaded to Shipbook, or you're experiencing some other issue with Shipbook, you can enable Shipbook debug logging to track down the problem.

  Shipbook.enableInnerLog(true);

Linking ShipBook to a user's information​

The SDK allows the option to associate each session with specific user information.

Register user:​

The best practice is to set registerUser before Shipbook.start. It will also work after this point however, it will require an additional api request.

  Shipbook.registerUser("USER_ID", "USER_NAME", "FULL_NAME", "USER_EMAIL", "USER_PHONE_NUMBER", { role: "admin" });

The only required parameter is userId.

Logout​

To logout the user, add the following code to your app's logout function.

  Shipbook.logout();

Screen​

To log the user's screen information, add the following code

  Shipbook.screen("SCREEN_NAME");

Flush​

Logs are sent in batches, every few seconds. To send everything waiting right now:

  await Shipbook.flush();

Get the UUID​

The SDK creates it's own UUID. So to get the UUID call the following code.

  Shipbook.getUUID()

Express Middleware​

Shipbook provides an Express middleware for grouping logs by HTTP request:

  import { Shipbook } from '@shipbook/node';
import express from 'express';

const app = express();
app.use(Shipbook.expressMiddleware());

NestJS Interceptor​

For NestJS applications, use the built-in interceptor:

  import { Shipbook } from '@shipbook/node';

// In your module or main.ts
app.useGlobalInterceptors(Shipbook.nestjsInterceptor());

Request Attributes​

Each request records its path, IP address and user agent, and the Logbook shows them in the request's header. Add your own facts with an attributes extractor (string, number or boolean values); they are shown there too:

  app.use(Shipbook.expressMiddleware({
attributes: (req) => ({ tenant: req.headers['x-tenant'] })
}));

// NestJS
app.useGlobalInterceptors(Shipbook.nestjsInterceptor({
attributes: (ctx) => ({ tenant: ctx.switchToHttp().getRequest().headers['x-tenant'] })
}));

MCP Servers​

If your Node.js app is an MCP server, each MCP call shows in the Logbook as its own request — the tool, prompt, or resource it was asked for (tools/call get-logs), how long it took, and whether it failed — with the logs your tool wrote grouped under it.

  • MCP over HTTP (Streamable HTTP behind Express or NestJS): nothing to add — the middleware or interceptor above already labels each call.
  • MCP over stdio, WebSocket, or any other transport: wrap the transport when you connect your server:
  import { Shipbook } from '@shipbook/node';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

await server.connect(Shipbook.mcpTransport(new StdioServerTransport()));

Each MCP connection is one session. A call answered with a JSON-RPC error is marked failed, with its error code (for example error -32602).

Background Jobs​

For background jobs and workers, wrap each run:

  await Shipbook.runInContext({ jobName: 'send-emails', attributes: { batch: batchId } }, async () => {
// All logs inside here are grouped under this job
log.i('Processing email batch');
});

Each run is its own session and counts as one request, like an HTTP request: it appears in the request counts and error rates, and the Logbook shows when it started, how long it took, and whether it threw. attributes is optional: the job's own facts, shown on its session.

Other Units of Work​

For work that isn't an HTTP request — a queue message, a WebSocket message — runInRequest reads it as one request: its logs are grouped together, with how long it took and whether it failed (a thrown error marks it failed).

  import { Shipbook, RequestKind } from '@shipbook/node';

queue.on('message', (message) =>
Shipbook.runInRequest(RequestKind.Rpc, 'process-order', async () => {
log.i(`Processing order ${message.orderId}`);
}));

Crashes and Shutdown​

The SDK sends what it has before the process ends, with no setup:

  • An uncaught exception or unhandled promise rejection is recorded as an exception and sent. The process then exits as Node would have, with the error printed.
  • On SIGTERM or SIGINT (a deploy, Ctrl+C), pending logs are sent, then the process ends as it would have.

If your app has its own handler for one of these events, the SDK only sends the logs and leaves the exit to your handler.

When you stop the SDK yourself, call shutdown. It sends pending logs and stops listening:

  await Shipbook.shutdown();

Using Wrappers with ShipBook​

If you wrap ShipBook in your own logging class (e.g., a custom Logger or Winston integration), the SDK will report your wrapper's filename and line number instead of the actual caller. To fix this, register your wrapper class:

  // Pass the class itself (recommended - works with minification)
Shipbook.addWrapperClass(MyLogWrapper);

// Or pass a string for third-party internals
Shipbook.addWrapperClass('WinstonInternalClass');

The SDK will skip any stack frames from registered classes when determining the caller location.

Stack Offset​

For cases where addWrapperClass isn't sufficient (e.g., deeply nested third-party wrappers in minified code), you can set a manual stack offset:

  // Skip 1 additional stack frame beyond SDK internals
Shipbook.setStackOffset(1);

Custom Appenders​

You can create custom appenders to route logs to additional destinations such as analytics services, local databases, or test harnesses. Register your custom appender before calling start().

class AnalyticsAppender {
constructor(name, config) {
this.name = name;
}

update(config) {}

push(log) {
if (log.type === 'message') {
// Send to your analytics service
}
}

flush() {}
destructor() {}
}

// Register before start
Shipbook.registerAppender('AnalyticsAppender', AnalyticsAppender);

await Shipbook.start('YOUR_APP_ID', 'YOUR_APP_KEY');

Once registered, your custom appender can be configured through the Shipbook console by its type name, and assigned to loggers with the desired severity level.