Connect a Sails app
Add ApiTraffic to a Sails API as a hook: install, credentials, what is recorded, firewall, APM, tagging, tracing and troubleshooting.
@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
npm install @apitraffic/sails
yarn
yarn add @apitraffic/sails
pnpm
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.jsondependencies, whichnpm installdoes 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:
// 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:
// 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.
// 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();
};
// 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:
// 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:
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:
node -r @apitraffic/sails/register app.js
or call register() on the very first line of app.js, ahead of every other require:
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.
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:
// 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.