@gorilli-team/bananaloop-catch
v0.3.0
Published
Lightweight Node.js error capture SDK with Express and Next.js integrations.
Readme
@gorilli-team/bananaloop-catch
Lightweight Node.js error capture SDK. Automatically reports unhandled exceptions and promise rejections, with built-in integrations for Express and Next.js.
Requirements
- Node.js 18+
- An ingest server URL and a per-project ingest token. Token format:
bnl_<id>_<secret>.
Installation
npm install @gorilli-team/bananaloop-catch
# or
pnpm add @gorilli-team/bananaloop-catch
# or
yarn add @gorilli-team/bananaloop-catchQuick Start
Call init() once at the entry point of your application, before any other code runs:
import { init } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});The per-project token identifies the project and workspace server-side — no projectId config field is required. After this, all uncaught exceptions and unhandled promise rejections are automatically captured and sent to your ingest server.
Environment Variables
The SDK reads these environment variables for runtime context. Setting them is optional; you can also pass equivalent values via code.
| Variable | Description |
|---|---|
| BANANALOOP_GIT_SHA | Git commit SHA to attach to every error report. Falls back to git rev-parse --short HEAD. |
| BANANALOOP_GIT_BRANCH | Git branch name to attach to every error report. Falls back to git rev-parse --abbrev-ref HEAD. |
| BANANALOOP_TASK_ID | Task ID associated with the running process. |
| BANANALOOP_WORKSPACE_ID | Workspace ID associated with the running process. |
| BANANALOOP_DAEMON_ID | Daemon / runner ID associated with the running process. |
BANANALOOP_INGEST_TOKEN in the Quick Start is just an example name — the SDK does not read it automatically. Pass its value to init() via apiKey.
Configuration
init() accepts a CatchConfig object:
init({
apiKey: 'bnl_...', // required — per-project ingest token
apiUrl: 'https://ingest.example.com', // optional — base URL of the ingest server
// projectId: 'your-project-id', // deprecated — only needed for legacy global keys
});Deprecated:
projectIdis ignored whenapiKeyis a per-project token (bnl_*). It is only honored whenapiKeyis a legacy global ingest key, which will be removed in a future release.
Reports are POSTed to ${apiUrl}/api/errors/ingest with the apiKey as a bearer token.
API Reference
init(config: CatchConfig): void
Initialises the global Catch client and attaches uncaughtException / unhandledRejection process handlers. Safe to call multiple times — handlers are only attached once.
import { init } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});Config options:
| Field | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Per-project ingest token (bnl_<id>_<secret>). |
| apiUrl | string | No | Base URL of the ingest server. Defaults to the built-in value. |
| projectId | string | No | Legacy — only honored when apiKey is a global key. |
getContext(): RuntimeContext | null
Returns the RuntimeContext collected at init time, or null if init() has not been called.
import { getContext } from '@gorilli-team/bananaloop-catch';
const ctx = getContext();
console.log(ctx?.gitBranch); // e.g. "main"RuntimeContext shape:
interface RuntimeContext {
gitSha: string | null;
gitBranch: string | null;
hostname: string;
nodeVersion: string;
taskId: string | null;
workspaceId: string | null;
daemonId: string | null;
}createCatchClient(config, context?): CatchClient
Creates a standalone CatchClient instance without touching global state. Use this when you need multiple clients or want to avoid global side-effects.
import { createCatchClient, collectContext } from '@gorilli-team/bananaloop-catch';
const client = createCatchClient(
{
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
},
collectContext(),
);
await client.captureError(new Error('Something went wrong'));CatchClient interface:
interface CatchClient {
captureError(error: Error, context?: Record<string, unknown>): Promise<void>;
captureException(error: unknown, context?: Record<string, unknown>): Promise<void>;
}captureError— accepts anErrorinstance directly.captureException— accepts any thrown value; non-Errorvalues are converted vianew Error(String(value)).
collectContext(): RuntimeContext
Reads environment variables and system info to build a RuntimeContext. Called internally by init().
import { collectContext } from '@gorilli-team/bananaloop-catch';
const ctx = collectContext();detectGitSha(): string | null
Returns the current git commit SHA (short form). Prefers BANANALOOP_GIT_SHA env var, falls back to running git rev-parse --short HEAD. Returns null if neither is available.
import { detectGitSha } from '@gorilli-team/bananaloop-catch';
const sha = detectGitSha(); // e.g. "a1b2c3d"detectGitBranch(): string | null
Returns the current git branch name. Prefers BANANALOOP_GIT_BRANCH env var, falls back to running git rev-parse --abbrev-ref HEAD. Returns null if neither is available.
import { detectGitBranch } from '@gorilli-team/bananaloop-catch';
const branch = detectGitBranch(); // e.g. "main"createExpressErrorHandler(client: CatchClient): ErrorRequestHandler
Creates an Express 4-argument error-handling middleware that captures errors via a provided CatchClient instance. Always calls next(err) to continue the Express error chain.
import express from 'express';
import { createCatchClient, createExpressErrorHandler, collectContext } from '@gorilli-team/bananaloop-catch';
const app = express();
const client = createCatchClient(
{
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
},
collectContext(),
);
app.use(createExpressErrorHandler(client));captureExpressErrors(): ErrorRequestHandler
Returns Express error-handling middleware that uses the global client initialised by init(). Register as your last error middleware.
import express from 'express';
import { init, captureExpressErrors } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});
const app = express();
// ... your routes ...
app.use(captureExpressErrors()); // must be lastwithCatch(client: CatchClient, handler): Handler
Wraps a Next.js App Router route handler. Exceptions are captured and re-thrown so Next.js can still handle its own error boundary / 500 response logic.
import { withCatch } from '@gorilli-team/bananaloop-catch';captureNextErrors(err: unknown, req?: NextCompatibleRequest): void
Captures an error from a Next.js error page. Safe to call from any rendering context — never throws. Accepts both App Router (Request) and Pages Router (IncomingMessage) request objects.
Framework Integration Examples
Plain Node.js
import { init } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});
// Unhandled errors are now auto-captured.
// You can also capture manually:
import { createCatchClient, collectContext } from '@gorilli-team/bananaloop-catch';
const client = createCatchClient(
{
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
},
collectContext(),
);
try {
riskyOperation();
} catch (err) {
await client.captureException(err, { userId: '123', action: 'riskyOperation' });
throw err;
}Express
import express from 'express';
import { init, captureExpressErrors } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});
const app = express();
app.get('/example', (req, res) => {
res.json({ ok: true });
});
// Register error handler last — after all routes and other middleware.
app.use(captureExpressErrors());
app.listen(3000);The middleware automatically attaches safe request metadata (URL, method, selected headers) to each captured error.
Next.js — App Router
instrumentation.ts (runs once at startup):
export async function register() {
const { init } = await import('@gorilli-team/bananaloop-catch');
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});
}Route handlers (app/api/example/route.ts):
import { createCatchClient, collectContext, withCatch } from '@gorilli-team/bananaloop-catch';
import { NextResponse } from 'next/server';
const client = createCatchClient(
{
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
},
collectContext(),
);
export const GET = withCatch(client, async (_req) => {
// errors thrown here are captured and re-thrown for Next.js to handle
return NextResponse.json({ ok: true });
});Error boundary (app/error.tsx):
'use client';
import { useEffect } from 'react';
import { captureNextErrors } from '@gorilli-team/bananaloop-catch';
export default function ErrorPage({ error }: { error: Error }) {
useEffect(() => {
captureNextErrors(error);
}, [error]);
return <div>Something went wrong.</div>;
}Next.js — Pages Router
pages/_app.tsx:
import type { AppProps } from 'next/app';
import { init } from '@gorilli-team/bananaloop-catch';
init({
apiKey: process.env.BANANALOOP_INGEST_TOKEN!,
});
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}pages/_error.tsx:
import type { NextPageContext } from 'next';
import { captureNextErrors } from '@gorilli-team/bananaloop-catch';
function ErrorPage({ statusCode }: { statusCode?: number }) {
return <p>{statusCode} — An error occurred.</p>;
}
ErrorPage.getInitialProps = ({ res, err, req }: NextPageContext) => {
if (err) captureNextErrors(err, req ?? undefined);
return { statusCode: res?.statusCode ?? err?.statusCode ?? 404 };
};
export default ErrorPage;Error Handling Best Practices
Always initialise before other code. Call init() at the top of your entry file so process-level handlers are attached before any imports throw.
Use captureException for caught errors you still want to re-throw. It normalises non-Error values automatically:
try {
await thirdPartyCall();
} catch (err) {
await client.captureException(err, { service: 'thirdParty' });
throw err; // re-throw so callers know the operation failed
}Add structured context to help with debugging:
await client.captureError(err, {
userId: session.userId,
requestId: req.headers['x-request-id'],
operation: 'processPayment',
orderId: order.id,
});Never swallow errors silently. The SDK is fire-and-forget by design — network failures do not throw. Your application's own error handling (re-throwing, returning 500 responses) is still your responsibility.
Keep apiKey in environment variables. Never hardcode the ingest API key in source code.
Troubleshooting
Errors not reaching the ingest server
- Check that
init()is called before any errors can occur. - Verify
apiKeyis set and non-empty — the SDK logs a warning toconsole.warnif it is missing. - Confirm your service can reach the ingest
apiUrl(check firewall / egress rules). - The SDK has a 5-second timeout per request. In serverless environments with very short execution windows, the ingest request may be killed before it completes. Use
waitUntilor an equivalent mechanism if available.
[catch] Missing required config: apiKey
init() was called without a valid apiKey. Check that the ingest token is populated in your deployment environment.
[catch] Failed to send error to ingest: ...
A network error occurred while sending the report. The original error in your application is unaffected. Verify network connectivity and that apiUrl is correct.
[catch] Ingest request failed with status 401
The API key is invalid or has been revoked. Regenerate the key.
[catch] Ingest request failed with status 429
You are being rate limited. Reduce the volume of errors or contact your ingest provider.
Git context is null
Ensure the BANANALOOP_GIT_SHA / BANANALOOP_GIT_BRANCH environment variables are set in your CI/CD pipeline, or that a .git directory is present at runtime so the SDK can call git rev-parse.
TypeScript: module resolution errors
The package uses ESM ("type": "module"). Ensure your tsconfig.json uses "moduleResolution": "bundler" or "node16" / "nodenext" and that your build tool supports ESM.
TypeScript
The package ships with full TypeScript declarations. All types are exported from the main entry point:
import type { CatchConfig, CatchClient } from '@gorilli-team/bananaloop-catch';
import type { RuntimeContext } from '@gorilli-team/bananaloop-catch';
import type { NextCompatibleRequest } from '@gorilli-team/bananaloop-catch';