Skip to main content

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 dependenciesfetch, 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 levelsource type message severity stackTrace environment release context
contextruntime 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, never uncaughtException — 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. captureException returns 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/errors is 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.

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.