# Connect a Meteor app

> Add ApiTraffic to a Meteor server's WebApp: install, mount, credentials, what is recorded, firewall, APM, tagging and troubleshooting.

Source: https://www.apitraffic.io/docs/integrations/meteor

`@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`](https://www.apitraffic.io/docs/integrations/node), the one-install SDK, wired into Meteor:
Meteor 3's `WebApp` is an Express application, so requests are captured by
[`@apitraffic/express`](https://www.apitraffic.io/docs/integrations/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**

```bash
meteor npm install @apitraffic/meteor
```

**npm**

```bash
npm install @apitraffic/meteor
```

> **Note:** **Meteor 3 required** (it runs on Node 20 and later): 3.0, whose `WebApp` is 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

```javascript
// 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`:

```javascript
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](#configuration-options)),
`mount()` takes no arguments at all. Settings from `Meteor.settings` work as any other options do:

```javascript
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 `405` for 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](https://www.apitraffic.io/docs/endpoint-handling#framework-routes).
Requests are captured by `@apitraffic/express`: what the [Express](https://www.apitraffic.io/docs/integrations/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](#tagging-tracing-and-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:

```javascript
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](https://www.apitraffic.io/docs/api-reference/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`.)

```javascript
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:

```javascript
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.

```javascript
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`:

```javascript
mount({ waf: false });
```

See [Firewall setup](https://www.apitraffic.io/docs/waf/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:

```bash
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`:

```bash
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.

```typescript
import { mount, type ApiTrafficOptions } from '@apitraffic/meteor';

const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };
mount(options);
```

## Embed tokens

The [embeddable request log](https://www.apitraffic.io/docs/integrations/embed) authenticates with a token your server
mints. `embedToken` is exported here, as from every ApiTraffic package:

```javascript
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](#firewall)).

## Sample Application

A complete working example is available in the [GitHub repository](https://github.com/apitraffic/platform/tree/main/libs/client-libraries/apitraffic-meteor/examples/basic).
