JavaScript / TypeScript

The tinymonjs package works in browsers, Node, Deno, Bun, and Cloudflare Workers. It has zero runtime dependencies and ships as both ESM and CJS with TypeScript types.

Building a web app? Don't stop at init() — see Frameworks below to enable automatic 500-error capture for Express, Fastify, and Next.js in one line.

Install

$ npm install tinymonjs
# or:
$ pnpm add tinymonjs
$ yarn add tinymonjs
$ bun add tinymonjs

init()

Call init() once, as early as possible in your app's startup. It installs global handlers for window.onerror / unhandledrejection in browsers, and process.on('uncaughtException') / 'unhandledRejection' in Node.

import { init } from 'tinymonjs';

init({
  dsn:         process.env.TINYMON_DSN,
  environment: 'production',
  release:     process.env.RELEASE, // e.g. git short SHA, set at build
  sampleRate:  1.0,
});

Options

FieldTypeDescription
dsnstringRequired. Your project DSN, e.g. tm_pub_….
endpointstringOverride the ingest URL. Defaults to https://console.tinymon.dev/api/ingest.
environmentstringFree-form tag — typically production, staging, development.
releasestringA version string for your app — git SHA, semver, anything. Powers release tracking (first-seen-in-release, regression detection) and scopes source maps. Use the same value at deploy.
sampleRatenumber0 to 1. Fraction of events to send. Default 1 (send all).
beforeSend(e) => e | nullMutate or drop events before they go out. Return null to drop.

Capturing errors

Most errors are caught automatically. For errors you handle but still want to report, use captureException:

import { captureException, captureMessage } from 'tinymonjs';

try {
  riskyThing();
} catch (err) {
  captureException(err);
}

// Or a plain message, no exception object:
captureMessage('cron job took 28 seconds', 'warning');

captureMessage takes a level: 'error', 'warning', or 'info'.

Delivery & flush()

Each event is sent immediately when you capture it — there's no batching delay. If the network fails it's queued and retried with backoff; a successful send never touches the queue. You don't manage any of this.

The send is non-blocking, so your code keeps running while the request is in flight. In a long-running server or the browser that's exactly what you want and you never need to do anything else. The one exception is a process that exits right after capturing — a serverless function or a CLI script — which can tear down before the request finishes. There, call flush() to block until delivery completes (or the timeout elapses):

import { captureException, flush } from 'tinymonjs';

try {
  await runJob();
} catch (err) {
  captureException(err);
  await flush(2000); // wait up to 2s, then give up
  throw err;
}
Rule of thumb. Long-running server or browser → never call flush(). Serverless / short-lived script → call it once before the process exits. Full recipes (Lambda, Next.js, Cloudflare) are in Delivery & flush(); the mechanics are in Transport internals.

User & tag context

Attach a user identifier and arbitrary tags to subsequent events. Useful for filtering in the dashboard.

import { setUser, setTag } from 'tinymonjs';

setUser({ id: user.id });
setTag('plan', user.plan);
setTag('feature_flag.new_checkout', 'on');
Privacy. Only send identifiers you're comfortable storing. setUser takes { id } by design — no email, no name, no IP.

Breadcrumbs are short notes about what happened before an error. The SDK keeps the last 30; when an error fires, they're attached to the event.

import { addBreadcrumb } from 'tinymonjs';

addBreadcrumb({
  timestamp: Date.now() / 1000,
  category:  'http',
  message:   'POST /api/orders → 500',
  level:     'error',
});

Filtering with beforeSend

Drop noisy errors, redact fields, or sample by error type:

init({
  dsn: process.env.TINYMON_DSN,
  beforeSend: (event) => {
    // Drop ResizeObserver loop spam from old browsers.
    if (event.exception.value.includes('ResizeObserver')) return null;
    // Redact email-looking strings from the breadcrumbs.
    return event;
  },
});

Frameworks

React

Call init() in index.tsx before ReactDOM.createRoot. Wrap routes in an error boundary that calls captureException:

class ErrorBoundary extends React.Component {
  componentDidCatch(err) {
    captureException(err);
  }
  render() { return this.props.children; }
}

Next.js

Fastest: one command. The wizard auto-detects Next.js (14 or 15), creates every file, patches your layout.tsx and next.config, and installs the SDK:
$ npx tinymon-setup --dsn tm_pub_xxx
Prefer to wire it by hand? The full manual recipe is below.

App Router, no index.js? That's expected — Next.js has no single entry file. Set it up once across three files (server, client, render boundary) and every error surface is covered with no per-route code. Full recipe below.

Initialise the SDK server-side from instrumentation.ts at your project root:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME !== 'nodejs') return; // skip the Edge runtime
  const { init } = await import('tinymonjs');
  init({
    dsn: process.env.NEXT_PUBLIC_TINYMON_DSN,
    environment: process.env.NODE_ENV,
  });
}
Next.js 14 and earlier need an opt-in. instrumentation.ts only runs if you enable the hook in next.config.mjs. Without it register() never fires, init() is never called, and captureException is a silent no-op — errors never reach tinymon.
// next.config.mjs
const nextConfig = {
  experimental: { instrumentationHook: true },
};
This became the default in Next.js 15, where the flag is no longer required.

Next.js catches errors thrown from Route Handlers and Server Actions before they reach the global handler, so report those explicitly. Serverless invocations can suspend the moment the response is sent, so await flush() to guarantee the event is delivered before the function freezes:

import { captureException, flush } from 'tinymonjs';

export async function GET() {
  try {
    return await handler();
  } catch (err) {
    captureException(err);
    await flush(); // deliver before the invocation suspends
    throw err;
  }
}

For the browser side, call init() in a top-level client component. The NEXT_PUBLIC_TINYMON_DSN env var is inlined into the client bundle, so the same public DSN works on both sides.

Capture everything automatically (Next.js 15+)

On Next.js 15 you don't need to wrap a single route. Add onRequestError to instrumentation.ts — it fires for every server error (route handlers, server actions, and server components):

// instrumentation.ts — add alongside register()
export async function onRequestError(err) {
  const { captureException, flush } = await import('tinymonjs');
  captureException(err);
  await flush(2000);
}

For the client, create a top-level init component and render it in app/layout.tsx:

// app/tinymon-init.tsx
'use client';
import { useEffect } from 'react';
import { init } from 'tinymonjs';
export default function TinymonInit() {
  useEffect(() => {
    init({ dsn: process.env.NEXT_PUBLIC_TINYMON_DSN });
  }, []);
  return null;
}

And catch client render errors with the App Router error boundary:

// app/global-error.tsx
'use client';
import { useEffect } from 'react';
import { captureException } from 'tinymonjs';
export default function GlobalError({ error }) {
  useEffect(() => { captureException(error); }, [error]);
  return <html><body><p>Something went wrong</p></body></html>;
}
That's the whole setup. onRequestError (server) + TinymonInit (client) + global-error.tsx (render) = every error captured, zero per-route wrapping. On Next.js 14, onRequestError doesn't exist — keep the register() hook (with the instrumentationHook opt-in above) and wrap Route Handlers manually as shown.

Express

Use the built-in error handler — add it last, after your routes:

import { expressErrorHandler } from 'tinymonjs';

app.use(expressErrorHandler()); // captures, then passes the error along

Fastify

import { fastifyPlugin } from 'tinymonjs';

app.register(fastifyPlugin);

Cloudflare Workers

Workers don't have a global process. Pass the DSN as a binding and call captureException manually in your handler's catch block. Add ctx.waitUntil(...) so the event flushes before the request ends.

Source maps

For minified browser bundles, upload source maps so stack traces are readable. Use the bundled CLI — see Source maps for the full guide:

npx tinymon-sourcemaps --url https://console.tinymon.dev --token $TINYMON_UPLOAD_TOKEN --release "$(git rev-parse --short HEAD)" --path ./dist

The same release string must be passed to init() — mismatched releases never silently resolve against the wrong map.

Don't ship sourcemaps to users. Either upload to tinymon and serve them with X-Content-Source-Map from a non-public URL, or strip the //# sourceMappingURL= comment from the production bundle.