Node.js and NestJS SDK
@store-pilot/node reports your backend's exceptions to StorePilot. It is the third way to
install StorePilot, beside the WordPress plugin and the loader tag, and it is for the half of a
shop that has no browser in it — an API, a worker, a checkout service.
npm install @store-pilot/node
Requires Node 18 or newer. It has no runtime dependencies — fetch,
AbortSignal.timeout and AsyncLocalStorage all come from Node itself, so nothing of ours is
added to your production dependency tree. The /nestjs subpath uses @nestjs/common,
@nestjs/core and rxjs, which every Nest service already has; all three are optional peers,
so a plain Express or Fastify service installs none of them.
Getting your key
Dashboard → Settings → Installation → Server SDK. It is the site's secret key — the same one the WordPress plugin uses, and the same one a build pipeline uses to upload source maps. It stays on your server and is never sent to a browser.
There is deliberately no public-key route for a server. The public key is authenticated by
Origin, which names a page; a Node process has none, so a browser-reachable route would let
anybody who can read a public key out of a <script> tag invent server-side issues in your
dashboard.
Node
const storepilot = require('@store-pilot/node');
storepilot.init({
key: process.env.STOREPILOT_KEY,
release: process.env.GIT_SHA, // optional, and what source maps key on
environment: process.env.NODE_ENV, // optional; the site's own setting is the fallback
});
try {
await chargeCard(order);
} catch (error) {
storepilot.captureException(error);
throw error;
}
// Drain before the process goes away.
process.on('SIGTERM', () => storepilot.flush(2000).then(() => process.exit(0)));
NestJS
import { StorePilotModule } from '@store-pilot/node/nestjs';
@Module({
imports: [
StorePilotModule.forRoot({
key: process.env.STOREPILOT_KEY!,
}),
],
})
export class AppModule {}
The forRoot above is the whole installation — there is nothing to add to main.ts. It binds
a global APP_INTERCEPTOR that watches every request, reports anything that is not an
HttpException and any HttpException at 500 or above, and then lets the error continue into
Nest's own exception layer untouched. An interceptor observes; it does not answer. Your own
filters keep running and the response your client receives is unchanged.
A 4xx is the API working — a validation failure or a missing row is an answer, not a fault — and reporting them would bury a real 500 under ordinary traffic.
A platform of your own
The platform address ships with the package — https://api.store-pilot.net — so the hosted
platform needs no address at all, exactly as the WordPress plugin carries its own. Set url for
a self-hosted backend, a staging platform, or local development:
storepilot.init({ key: process.env.STOREPILOT_KEY, url: 'https://storepilot.internal' });
It is deliberately not a way to switch reporting off. With url unset the SDK posts to the
hosted platform, so url: process.env.STOREPILOT_URL with the variable missing reports to
production rather than falling silent. Off is enabled: false, or no key — and init says
which, once, in the log.
Guards and middleware
An interceptor never runs for an exception thrown in a guard or in middleware: both run before it. To cover those as well, register the filter through the injector:
import { APP_FILTER } from '@nestjs/core';
import { StorePilotExceptionFilter } from '@store-pilot/node/nestjs';
@Module({
providers: [{ provide: APP_FILTER, useClass: StorePilotExceptionFilter }],
})
export class AppModule {}
It has to be registered that way — or with the adapter passed by hand,
new StorePilotExceptionFilter(app.getHttpAdapter()) — because it answers through Nest's own
responder, and Nest hands that to a filter only through dependency injection.
It is a catch-all, and Nest lets exactly one global filter answer: the last one registered.
So registering it displaces another global @Catch() filter of your own. That is Nest's rule
rather than ours, and it is why the module does not bind it for you. Either way a fault is
reported once, by whichever layer saw it first.
Upgrading from 0.1.x
Delete app.useGlobalFilters(new StorePilotExceptionFilter()) from main.ts. In 0.1.x that
filter re-threw, and NestJS does not catch what a filter throws: on Express 4 the request was
never answered at all, and on Express 5 it was answered by Express's own error page with your
filters skipped.
StorePilotModule is also available as forRootAsync({ imports, inject, useFactory }) when the
key comes from your own ConfigService.
What is sent, exactly
| Top level | source type message severity stackTrace environment release context |
context | runtime nodeVersion route method statusCode durationMs handled file |
That table is the whole payload.
The SDK never sends a request body, request headers, a query string, cookies, an
authenticated user, or an IP address. route is the route template (/orders/:id), never
the address, so an identifier in a URL does not reach us; a request that matched no route
reports the literal <unrouted>.
The payload is built by naming what goes rather than by copying a request object and removing what should not: a deny-list over somebody else's data structure is a promise nobody can keep.
What it will not do to your service
- It does not stop your process from crashing. The SDK listens on
uncaughtExceptionMonitor, neveruncaughtException— the latter cancels Node's default handling, so a service that used to die and be restarted would instead carry on in an undefined state. An unhandled rejection keeps whatever mode your process was started with. - It never blocks a request.
captureExceptionreturns immediately, never throws and never rejects. An unreachable platform costs a log line and nothing else. - It is bounded: one report per fault per 5 minutes, at most 30 reports a minute, at most 30
queued.
/ingest/errorsis rate limited per site, so your crashing worker must not spend the budget your checkout exception needs. - It does not report itself. A fault whose every file-naming stack frame is inside the package is dropped rather than sent — and one frame of your own service makes the whole stack yours, so a fault thrown through the SDK is still reported.
- It does not answer your requests. On NestJS the capture path is an interceptor: it observes the error and re-emits it unchanged, so Nest's exception layer and your own filters decide the response exactly as they did before.
On a hard crash the process is gone before the request finishes, so those reports are
best-effort. The reliable paths are the Nest interceptor — which sees every request the
service handles, and which StorePilotModule binds itself — and a flush() on SIGTERM.
Consent
If the site has Require consent switched on in Settings → Privacy, server-side reports are not stored at all. A process has no visitor and cannot assert consent on anybody's behalf, and the platform refuses a report that does not carry one. The same is true of the WordPress plugin's PHP error reports.
Source maps
Your stack arrives as compiled dist/*.js. Upload the maps with the same key and the platform
symbolicates at read time — see Source maps. Use a distinct release
string per runtime if one site reports from both a browser bundle and a Node service.
How the site is labelled
The SDK sends X-StorePilot-SDK: node/<version> alongside the key, and that is what records
the site's install method as Server SDK rather than as a WordPress plugin. It matters:
without it the platform would try to push notification settings to a wp-json route your
service does not have.
Once a site is recorded as a server install it stays one — a later request with no SDK header (your build pipeline uploading source maps, for instance) never relabels it.
Errors from a server look different on screen
A server-side report has no visit attached to it, so the issue page says so rather than drawing an empty timeline: there are no linked recordings, no visitor breakdown and no trail. The counts on the issue are the complete record.
"Where in the code" will read other for every issue. That facet describes a WordPress install — theme, plugin, core — and the Issues page says so when a site reports only from its own server.
Licence
The SDK is proprietary and ships its own LICENSE.md: you may install it and use it to send
data to StorePilot under a key issued to you, for as long as you hold a subscription for that
site. It may not be redistributed or pointed at another service. See
Terms of Service §11.
The WordPress plugin is licensed differently — GPL-2.0-or-later — because a WordPress plugin is a derivative work of WordPress. That licence covers the plugin's own code and reaches neither this SDK nor the platform.