Connect a Meteor app
Add ApiTraffic to a Meteor server's WebApp: install, mount, credentials, what is recorded, firewall, APM, tagging and troubleshooting.
@apitraffic/meteor mounts ApiTraffic on Meteor’s WebApp. Call mount() once, from your
server’s main module, and every HTTP request your Meteor server serves is recorded and checked by
the ApiTraffic firewall: your REST endpoints in WebApp.handlers, the handlers packages add, static
assets, and the app’s page itself.
It is @apitraffic/node, the one-install SDK, wired into Meteor:
Meteor 3’s WebApp is an Express application, so requests are captured by
@apitraffic/express, with the same firewall and APM bridge and the
same helpers, all enabled from your ApiTraffic account rather than in code.
Installation
meteor npm
meteor npm install @apitraffic/meteor
npm
npm install @apitraffic/meteor
Note: Meteor 3 required (it runs on Node 20 and later): 3.0, whose
WebAppis built on Express 4, and 3.1 and later, on Express 5. Meteor 2 runs on Node 14, which ApiTraffic does not support.
Basic Setup
// server/main.js
import { mount } from '@apitraffic/meteor';
mount({
token: process.env.API_TRAFFIC_TOKEN,
bucket: process.env.API_TRAFFIC_BUCKET,
});
Call it on the server, as early in your main module as you can. mount() finds the running
server’s WebApp itself; to be explicit, pass the one from meteor/webapp:
import { WebApp } from 'meteor/webapp';
import { mount } from '@apitraffic/meteor';
mount({ token: process.env.API_TRAFFIC_TOKEN, bucket: process.env.API_TRAFFIC_BUCKET }, WebApp);
It mounts once per WebApp, however many times it is called, and returns whether that call
mounted. With every option in the environment (see Configuration),
mount() takes no arguments at all. Settings from Meteor.settings work as any other options do:
import { Meteor } from 'meteor/meteor';
import { mount } from '@apitraffic/meteor';
mount(Meteor.settings.apiTraffic);
What is recorded
Every HTTP request WebApp serves, including the ones your code never sees. Capture takes the
first place in WebApp.rawHandlers, ahead of the raw handlers packages added before your code ran,
and ahead of everything else Meteor does with a request: compression, the ROOT_URL_PATH_PREFIX
check, static files, WebApp.handlers and the app’s page. So these are all recorded, with their
final status and whatever your code tagged on the way:
- your endpoints in
WebApp.handlers, with the route they matched, - a request a package’s handler answers itself,
- a static asset from the client bundle or
public/, - the app’s page (the HTML Meteor serves for a client-side route), and Meteor’s
405for a method other than GET or HEAD on it, - a request whose handler throws, as the response Express’s error handling sent.
The route your WebApp.handlers matched is sent with each request, mounted routers and
ROOT_URL_PATH_PREFIX included, so router.get('/items/:itemId') mounted at /api/v1 is the
endpoint /api/v1/items/{itemId}. See Endpoint Handling.
Requests are captured by @apitraffic/express: what the Express page
describes under “What is captured” holds here too. The request body is the one your body parser
produced, as your handler had it when it responded. The response body is what your handler passed
to res.send() or res.json(), before Meteor’s compression. Meteor compresses every response the
client accepts compressed, so a body written with res.write() / res.end(body) reaches capture
as compressed chunks and is not recorded, as with compression in any Express application: send
your API responses with res.send() or res.json().
Note: DDP - Meteor methods and publications - travels over Meteor’s WebSocket, not through
WebApp, and is not recorded as requests. Use spans for timing inside the HTTP endpoints that call into the same code.
Behind a proxy or load balancer
The client address recorded with each request, and used by the firewall’s IP rules and rate
limits, is Express’s req.ip. Meteor’s HTTP_FORWARDED_COUNT does not set it: tell Express how
many proxies to trust, the same number, or every request is recorded as coming from your load
balancer:
import { WebApp } from 'meteor/webapp';
WebApp.expressApp.set('trust proxy', 1);
Static assets and the app’s page are recorded like any other request. To leave them out, add exclusions in your ApiTraffic account; no deploy is needed.
Tagging, tracing and spans
The helpers work anywhere a request is being handled: your handlers in WebApp.handlers, raw
handlers, and any code they call. (The examples on this page use Express’s body parser and
routers from express in your own dependencies: meteor npm install express.)
import express from 'express';
import { WebApp } from 'meteor/webapp';
import { tag, trace, span } from '@apitraffic/meteor';
import { Orders } from '/imports/api/orders';
WebApp.handlers.use(express.json());
WebApp.handlers.get('/api/orders/:id', async (req, res) => {
tag('accountId', req.get('x-account-id'));
trace(`Loading order ${req.params.id}`);
const order = await span('db.query', 'load order', () => Orders.findOneAsync(req.params.id));
res.json(order);
});
Outside a request (a Meteor method, a Meteor.startup callback, a cron 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
The SDK installs no signal handlers of its own, and Meteor has no shutdown hook to follow. To
deliver what was captured before the process exits (a redeploy sends SIGTERM), call shutdown()
from your own handler:
import { shutdown } from '@apitraffic/meteor';
process.once('SIGTERM', async () => {
await shutdown();
process.exit(0);
});
Firewall
The ApiTraffic firewall is mounted by mount(), 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 right after capture, at the head of WebApp.rawHandlers, so a request it
blocks never reaches a package, a static file or your code. Blocked requests are recorded with the
policy’s status and tagged waf.*.
Meteor parses no request bodies, so there the firewall sees none. If your endpoints parse bodies,
mount firewall() again after your body parser, so body rules see the parsed body. The parser
here is Express’s own, from express in your application’s dependencies (meteor npm install express): the copy Meteor’s webapp package bundles is not one your code can import.
import express from 'express';
import { WebApp } from 'meteor/webapp';
import { firewall } from '@apitraffic/meteor';
WebApp.handlers.use(express.json(), firewall());
A request is evaluated there only when it has a body the first evaluation did not see, and is never counted against rate limits twice.
To refuse the firewall locally whatever the dashboard says (change control, or an incident switch
that does not depend on the control plane), pass waf: false or set
API_TRAFFIC_WAF_ENABLED=false:
mount({ waf: false });
See Firewall setup for policies and rules.
Application Performance Monitoring (APM)
The APM bridge captures database (MongoDB included), cache, queue, GraphQL, gRPC, and
fetch/undici spans within each ApiTraffic request, in your production bundle. Install the OpenTelemetry peers it uses:
meteor 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 Meteor does: Meteor
loads its packages, MongoDB’s driver among them, before your main module runs, so calling
register() from your own code is too late for them. Preload it into the server bundle that
meteor build produces, where your npm dependencies live under programs/server/npm/node_modules:
cd bundle
node -r ./programs/server/npm/node_modules/@apitraffic/meteor/dist/register.js main.js
or, where you cannot change the command (Galaxy, a container’s entrypoint), set
NODE_OPTIONS="-r ./programs/server/npm/node_modules/@apitraffic/meteor/dist/register.js" with the bundle
directory as the working directory. Configuration comes from the API_TRAFFIC_* environment
variables.
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 mount() with no options at all reads its
configuration from the environment. An option passed in code wins over the environment.
The options are typed: ApiTrafficOptions lists every option the SDK accepts, a value of the wrong
type is a compile error, and so is a misspelled key in an object literal.
import { mount, type ApiTrafficOptions } from '@apitraffic/meteor';
const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };
mount(options);
Embed tokens
The embeddable request log authenticates with a token your server
mints. embedToken is exported here, as from every ApiTraffic package:
import { WebApp } from 'meteor/webapp';
import { embedToken } from '@apitraffic/meteor';
WebApp.handlers.get('/embed/token', async (req, res) => {
res.json(await embedToken({ filters: { tags: { customerId: 'cus_123' } }, ttl: 900 }));
});
Troubleshooting
No requests are recorded
Check that mount() runs on the server (in server/main.js, or a module it imports), and that
API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET are set. Set debug: true to see what the SDK is
doing: it says so when there is no WebApp to mount on.
Meteor methods and subscriptions are missing
They travel over DDP, on Meteor’s WebSocket, not as HTTP requests through WebApp, so they are
not recorded as requests.
Body rules do not match
Meteor parses no request bodies. Mount firewall() after your body parser (see
Firewall).
Sample Application
A complete working example is available in the GitHub repository.