Connect a Sails app

Add ApiTraffic to a Sails API as a hook: install, credentials, what is recorded, firewall, APM, tagging, tracing and troubleshooting.

This page as Markdown

@apitraffic/sails is a Sails hook. Install it and every request your application serves is recorded and checked by the ApiTraffic firewall: Sails loads installable hooks from your dependencies when it lifts, so there is no code to add.

It is @apitraffic/node, the one-install SDK, wired into Sails: the same capture, the same firewall and APM bridge, the same helpers, all enabled from your ApiTraffic account rather than in code.

Installation

npm

bash
npm install @apitraffic/sails

yarn

bash
yarn add @apitraffic/sails

pnpm

bash
pnpm add @apitraffic/sails

Note: Node 20+ and Sails 1.x required. Sails only loads a hook that is listed in your app’s package.json dependencies, which npm install does for you.

Basic Setup

Set API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET in the environment and lift as usual. To configure the hook in code instead, add config/apitraffic.js:

javascript
// config/apitraffic.js
module.exports.apitraffic = {
  token: process.env.API_TRAFFIC_TOKEN,
  bucket: process.env.API_TRAFFIC_BUCKET,
};

Like any Sails config, it can be set per environment (config/env/production.js) or in config/local.js.

What is recorded

Every request, including the ones an action never sees. The hook adds an apitraffic entry to the front of sails.config.http.middleware.order, ahead of the cookie parser, the session, the body parser and the router, so these are all recorded, with their final status and whatever your code tagged on the way:

  • a request a policy rejects (401, 403),
  • a request your HTTP middleware answers itself,
  • a request no route matches (404),
  • a request whose action throws, as the response res.serverError() sent.

The route Sails matched is sent with each request, so 'GET /users/:id' in config/routes.js is the endpoint /users/{id}. See Endpoint Handling.

Sails serves HTTP through Express, and requests are captured by @apitraffic/express: exactly what the Express page describes under “What is captured” holds here too. Socket.io requests to your actions (io.socket.get()) do not go through Sails’ HTTP middleware: they are neither recorded nor checked by the firewall.

Placing it yourself

The hook keeps an apitraffic (or apitrafficFirewall) entry you list in order where you put it, for middleware that must run first, such as a health check you do not want recorded:

javascript
// config/http.js
module.exports.http = {
  middleware: {
    order: [
      'healthCheck',
      'apitraffic',
      'cookieParser',
      'session',
      'bodyParser',
      'apitrafficFirewall',
      'compress',
      'poweredBy',
      'router',
      'www',
      'favicon',
    ],
    healthCheck: (req, res, next) => (req.path === '/health' ? res.status(200).send('ok') : next()),
  },
};

ApiTraffic already in config/http.js

An application that already mounts ApiTraffic middleware itself as apitraffic in config/http.js (@apitraffic/express’s middleware(), say) keeps it after installing this package: the hook sees that apitraffic is the application’s own, logs that it does not set ApiTraffic up, and leaves it alone, along with any apitrafficFirewall entry, which then does nothing. To switch to the hook, delete the apitraffic function from config/http.js and move its options to config/apitraffic.js; the entries in order can stay where they are.

Tagging, tracing and spans

The helpers work anywhere a request is being handled: actions, helpers, policies and HTTP middleware.

javascript
// api/policies/is-account.js
const { tag } = require('@apitraffic/sails');

module.exports = async function (req, res, proceed) {
  // Searchable on this request, whether or not the policy lets it through.
  tag('accountId', req.headers['x-account-id']);
  if (!req.headers['x-account-id']) return res.forbidden();
  return proceed();
};
javascript
// api/controllers/user/find.js
const { span, trace } = require('@apitraffic/sails');

module.exports = async function find(req, res) {
  trace(`Loading user ${req.param('id')}`);
  const user = await span('db.query', 'load user', () => User.findOne({ id: req.param('id') }));
  return res.json(user);
};

Outside a request (a script, a bootstrap function, a scheduled job) the helpers do nothing and never throw.

stats() reports the SDK’s delivery health (delivered, queued, dropped), for a health endpoint or a log line.

Shutdown

sails.lower(), which Sails also runs on Ctrl-C and SIGTERM, delivers what was captured before Sails closes the server, after your own beforeShutdown, and releases the SDK’s timers and connections once the last application using the hook has lowered.

On a signal Sails exits the moment its server has closed, so a request still being served when lowering began is recorded only if the process stays up long enough to deliver it. To let them finish first, stop sending traffic to the instance (drain it at the load balancer) before you signal it.

Firewall

The ApiTraffic firewall is mounted by the hook, and whether it runs is decided in your ApiTraffic account: nothing is loaded until you switch a policy on, and a policy change reaches every server without a deploy. It runs as the apitrafficFirewall entry right after bodyParser, so body rules see the parsed body, and ahead of your policies and actions, so a request it blocks never reaches your code. Blocked requests are recorded with the policy’s status and tagged waf.*. It guards HTTP requests only: Socket.io requests reach your policies and actions without passing through it. Without a bodyParser in order, it goes just ahead of the router.

To refuse it locally whatever the dashboard says (change control, or an incident switch that does not depend on the control plane), set waf: false or API_TRAFFIC_WAF_ENABLED=false:

javascript
// config/apitraffic.js
module.exports.apitraffic = { waf: false };

See Firewall setup for policies and rules.

Application Performance Monitoring (APM)

The APM bridge captures database, cache, queue, GraphQL, gRPC, and fetch/undici spans within each ApiTraffic request. Install the OpenTelemetry peers it uses:

bash
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node

OpenTelemetry patches modules as they are loaded, so it has to start before Sails and your database adapters do. Preload it:

bash
node -r @apitraffic/sails/register app.js

or call register() on the very first line of app.js, ahead of every other require:

javascript
require('@apitraffic/sails').register();

If your application already configures OpenTelemetry, add ApiTrafficSpanProcessor from @apitraffic/apm to that provider instead, so spans are not double-instrumented.

Configuration Options

Option Environment Variable Required Type Description
token API_TRAFFIC_TOKEN Yes String Ingest token from your ApiTraffic account
bucket API_TRAFFIC_BUCKET Yes String Bucket ID for data organization
interceptOutbound API_TRAFFIC_INTERCEPT_OUTBOUND No Boolean Monitor outbound HTTP requests (default: true)
debug API_TRAFFIC_DEBUG No Boolean Enable debug logging (default: false)
waf API_TRAFFIC_WAF_ENABLED No Boolean or object false refuses the firewall locally; an object passes settings to @apitraffic/waf. Normally left unset: the firewall is switched on from your account

Every option falls back to its environment variable, so with no config/apitraffic.js at all the hook reads its configuration from the environment. An option in config wins over the environment.

apitraffic: false (module.exports.apitraffic = false in config/env/test.js, say) turns the hook off. sails.config.hooks.apitraffic = false does too, by not loading it at all, so use it only when config/http.js does not list apitraffic or apitrafficFirewall: Sails will not lift with an entry in order that nothing provides.

The options are typed, for a TypeScript Sails app: ApiTrafficOptions lists every option the SDK accepts.

typescript
import type { ApiTrafficOptions } from '@apitraffic/sails';

export const apitraffic: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };

Embed tokens

The embeddable request log authenticates with a token your server mints. embedToken is exported here, as from every ApiTraffic package:

javascript
// api/controllers/embed/token.js
const { embedToken } = require('@apitraffic/sails');

module.exports = async function token(req, res) {
  return res.json(await embedToken({ filters: { tags: { customerId: 'cus_123' } }, ttl: 900 }));
};

Troubleshooting

No requests are recorded

Check that @apitraffic/sails is listed in your app’s package.json dependencies (Sails ignores hooks that are not) and installed in the app’s own node_modules (see the monorepo note below), that neither sails.config.apitraffic nor sails.config.hooks.apitraffic is false, and that API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET are set. Set debug: true to see what the SDK is doing; when the hook is mounted, sails.config.http.middleware.order contains apitraffic (first, unless you placed it yourself) and apitrafficFirewall. If config/http.js defines its own apitraffic middleware, the hook does not set ApiTraffic up (see “ApiTraffic already in config/http.js” above).

The hook is not loaded in a monorepo

Sails looks for installable hooks only in the app’s own node_modules. If your package manager hoists @apitraffic/sails to the workspace root, Sails does not find it: add api/hooks/apitraffic/index.js with module.exports = require('@apitraffic/sails'); and Sails loads it from there, configured the same way.

Tags set in a script or scheduled job are missing

Tags, traces and spans belong to an HTTP request. Code that does not run while a request is being handled has none to attach them to, and the helpers do nothing there.

Socket.io requests are missing

Requests over Sails’ WebSocket transport are virtual: they reach your actions without passing through the HTTP server or its middleware, so there is nothing for the hook to record, and the firewall does not check them.

Sample Application

A complete working example is available in the GitHub repository.