Node.js SDK
Package @linearjeph/sdk 1.0.0, MIT licensed, Node 18+ (uses the global fetch), no dependencies. Ships CommonJS, ES modules and TypeScript types.
Install
npm install https://linearjeph.cc/sdk/linearjeph-sdk-1.0.0.tgzQuickstart
const linearjeph = require("@linearjeph/sdk"); // or: import * as linearjeph from "@linearjeph/sdk";
linearjeph.init({ key: process.env.LINEARJEPH_KEY, release: "1.4.2", environment: "production" });
try { risky(); } catch (err) { linearjeph.captureException(err); }Get the key from Dashboard > Maintenance > your application > Reveal key. init never throws; it returns true when the SDK is enabled. You can also pass the key alone: init("lj_app_...").
By default init installs process handlers (handlers: true): an uncaught exception or unhandled promise rejection is captured, flushed for up to 2 seconds, printed to stderr and the process exits with code 1, as Node would do on its own. If your app already has its own uncaughtException listener, the SDK only observes (through uncaughtExceptionMonitor) and does not change your behaviour. The SDK does not add an unhandledRejection listener, so Node's default crash-on-rejection stays in force (run with --unhandled-rejections=warn and rejections will not be captured automatically). A beforeExit hook flushes the queue.
init(options)
| Option | Type | Default | Meaning |
|---|---|---|---|
key | string | required | Application ingest key. |
release | string | none | Version or build identifier (max 100 chars). Drives release tracking. |
environment | string | "production" | Max 40 chars. |
endpoint | string | https://linearjeph.cc | Base URL; change only for a private deployment. |
sampleRate | number | 1 | Fraction of errors and messages sent, 0 to 1. |
tracesSampleRate | number | 1 | Fraction of transactions sent. |
beforeSend | (event) => event | null | none | Change the event or return null to drop it. If it throws, the event is dropped. |
scrubKeys | string[] | [] | Extra tag-key substrings to filter, case-insensitive. |
maxQueue | number | 100 | Items waiting to be sent; when full new items are dropped. |
batchSize | number | 20 | Events per request, 1 to 100. |
retries | number | 4 | Retries after the first attempt for 429, 5xx and network errors. |
backoffMs | number | 500 | Base delay; doubles each attempt with jitter. Retry-After on 429 is used instead (max 60 s). |
timeoutMs | number | 10000 | Per-request timeout. |
handlers | boolean | true | Install uncaught-exception capture and the beforeExit flush. |
exitOnUncaught | boolean | true | With handlers: after capture, flush and process.exit(1). |
flushTimeoutMs | number | 5000 | Longest wait for the flush in beforeExit. |
debug | boolean | false | Log SDK problems to stderr. |
Functions
| Function | Description |
|---|---|
captureException(err, { tags }?) | Report an error (or any thrown value). |
captureMessage(msg, level = "info", { tags }?) | Report a message as an event of type Message. |
captureTransaction(name, durationMs, status = "ok", { tags }?) | Report one timed operation. |
startTransaction(name) | Returns { end(status = "ok") }; only the first end counts. |
withTransaction(name, fn) | Times a sync or async function; status error if it throws (the error is rethrown, not captured). Returns the result. |
heartbeat(name, status = "ok", intervalS?) | Check in. intervalS is 30 to 2,592,000. name must match [A-Za-z0-9._-]{1,64}, otherwise ignored. |
monitor(name, fn, { intervalS?, capture?, flush? }?) | Run fn, send ok on success or error if it throws (then rethrow). capture: true also reports the error; flush: true waits for delivery. |
reportPackages(lockPath = ./package-lock.json) | Send the packages in package-lock.json (npm lockfile v1, v2 and v3) to the dependency audit. Returns the number queued, 0 if the file cannot be read. |
packagesFromLock(lockObject) | Parse a lockfile object into { name, version }[]. |
setTag(key, value) | Tag every later event (max 20 tags per event). |
setUser(id) | Attach a user id; null clears it. |
scrub(data) | Deep copy with sensitive values replaced by [Filtered]. |
flush(timeoutMs = 5000) | Promise resolving true when the queue is empty. |
requestHandler({ ignorePaths? }) | Express middleware that times requests (also exported as timing). |
errorHandler() | Express error middleware (also exported as expressErrorHandler). |
Express
const express = require("express");
const linearjeph = require("@linearjeph/sdk");
linearjeph.init({ key: process.env.LINEARJEPH_KEY, release: process.env.RELEASE });
const app = express();
app.use(linearjeph.requestHandler({ ignorePaths: ["/health"] })); // first
app.get("/items/:id", (req, res) => res.json({ id: req.params.id }));
app.use(linearjeph.errorHandler()); // after all routes
app.listen(3000);Transactions are named from the matched route (GET /items/:id), so ids do not create new names. errorHandler captures the error and passes it on with next(err), so your own error middleware still runs.
For Fastify, Koa and others, call startTransaction / captureException from your own hooks.
ESM and TypeScript
import { init, captureException, monitor, type InitOptions } from "@linearjeph/sdk";
const options: InitOptions = { key: process.env.LINEARJEPH_KEY!, release: "1.4.2" };
init(options);Types ship in index.d.ts; both import and require resolve through the package exports map.
Cron jobs and heartbeats
const linearjeph = require("@linearjeph/sdk");
linearjeph.init({ key: process.env.LINEARJEPH_KEY });
linearjeph.monitor("nightly-report", buildReport, { intervalS: 86400, capture: true, flush: true })
.catch(() => process.exitCode = 1);The first check-in creates the monitor. A failed run, or no check-in within the interval plus 5 minutes, opens an incident and sends alerts. For a scheduler inside a long-running process call linearjeph.heartbeat("worker", "ok", 60) on each tick.
Dependency audit
linearjeph.reportPackages(); // reads ./package-lock.json; call on startup or in CI, not per requestRun it from your build or deploy step with the same init call, then await linearjeph.flush().
Release tracking in CI
env:
RELEASE: ${{ github.ref_name }}+${{ github.run_number }}linearjeph.init({ key: process.env.LINEARJEPH_KEY, release: process.env.RELEASE });Sampling
linearjeph.init({ key, sampleRate: 0.5, tracesSampleRate: 0.1 });Sampled-out events never leave the process; sampling is applied before beforeSend.
Scrubbing and beforeSend
Tag values under keys containing password, secret, token, authorization, auth, cookie, api_key, session, csrf, bearer, credential or private_key (plus your scrubKeys) are replaced by [Filtered]. Text like password=hunter2 and Bearer eyJ... in error and message text is masked. The SDK never reads request headers, bodies or cookies.
linearjeph.init({
key,
scrubKeys: ["ssn"],
beforeSend(event) {
if (event.type === "error" && event.exception.type === "AbortError") return null;
event.tags.tenant = currentTenant();
return event;
},
});Shutdown
Short scripts and containers that exit through process.exit() skip beforeExit; call await linearjeph.flush() first. On SIGTERM handlers: process.on("SIGTERM", async () => { await linearjeph.flush(); process.exit(0); }).
Troubleshooting
- Use
debug: trueto see why events are dropped. - A 401 prints "ingest key rejected" once and stops sending.
initreturnsfalsewithout a key.- With
--unhandled-rejections=warnornone, unhandled rejections are not captured automatically; callcaptureExceptionin aprocess.on("unhandledRejection", ...)handler of your own. - See Troubleshooting and the Ingest HTTP API.