npm package: @soft-where/meter
Description: SOFT-WHERE usage metering library — production-safe, edge-compatible, stateless
Latest version: 1.0.3

# @soft-where/meter

Production-safe, edge-compatible usage metering for Node and Next.js. Tracks API requests, bandwidth, blob storage, custom metrics, and DB snapshots by posting to the SOFT-WHERE ERP. Stateless, non-blocking, and only active when `NODE_ENV === "production"`.

## Install

```bash
npm install @soft-where/meter
```

## Required variables

The meter needs three values. **Pass them in when calling `createMeter()`**; in apps you typically read from environment variables so secrets stay out of code.

| Variable | Purpose | Example |
|----------|---------|--------|
| **projectId** | Your SOFT-WHERE project identifier | `"proj_abc123"` |
| **apiKey** | Secret key for the ERP (do not expose to the client) | From SOFT-WHERE dashboard |
| **erpUrl** | Full URL of the SOFT-WHERE ERP ingest endpoint | `"https://erp.soft-where.com/ingest"` |
| **environment** | Optional label (e.g. staging vs production) | `"production"` |
| **development** | Optional. When `true`, logs every meter operation (success, failure, and skipped) to the terminal. | `true` in dev, omit or `false` in production |

**Important:** Do not use `NEXT_PUBLIC_*` for `apiKey` or other secrets. Use server/edge-only env vars (e.g. `SOFT_WHERE_API_KEY`) so they are not bundled for the browser.

## Usage

### 1. Create a meter

Call `createMeter()` once (e.g. at module load or in a shared helper) and reuse the returned object. Use env vars for config so you can change them per environment without code changes.

```ts
import { createMeter } from "@soft-where/meter";

const meter = createMeter({
  projectId: process.env.SOFT_WHERE_PROJECT_ID!,
  apiKey: process.env.SOFT_WHERE_API_KEY!,
  erpUrl: process.env.SOFT_WHERE_ERP_URL!,
  environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
  development: process.env.NODE_ENV !== "production", // verbose logging in dev
});
```

### 2. Use the meter

- **trackRequest(req, res, durationMs, bandwidthBytes?)** — Call with the same `Request` and `Response` (or Next.js `NextRequest` / `NextResponse`) and the request duration in milliseconds. The library sends one request event. By default it reads **bandwidth** from `Response#headers` `Content-Length`; that header is often missing in Next.js middleware (because the response body is produced after middleware runs), so bandwidth is then reported as 0. Pass the optional fourth argument **`bandwidthBytes`** when you know the response size (e.g. from a route handler or response wrapper) so the ERP gets accurate bandwidth.
- **trackBlobUpload(bytes)** / **trackBlobDelete(bytes)** — Call from upload/delete handlers with the byte deltas.
- **trackCustomMetric(name, value)** — Send arbitrary numeric metrics.
- **attemptDbSnapshot(getDbSize)** — Call with an async function that returns DB size in bytes (e.g. Neon HTTP). The library will call it, then post the snapshot to the ERP without blocking on the network.

All methods are no-ops when `NODE_ENV !== "production"`. They never throw and never block request completion (ERP calls are fire-and-forget with `keepalive: true`).

**Tracking bandwidth:** The library reports bandwidth from the response `Content-Length` header when present. In Next.js middleware the response from `NextResponse.next()` is created before the body exists, so `Content-Length` is usually missing and bandwidth is 0. To report accurate bandwidth, pass the response body size as the fourth argument: `trackRequest(req, res, durationMs, responseBodyBytes)`. You can supply this from API routes that know their payload size, or from any layer that measures the outgoing response (e.g. a wrapper that counts bytes).

### Development mode (verbose logging)

Pass **`development: true`** in `createMeter()` to enable verbose terminal logging for every meter operation:

- **✓ success** — Payload was sent and the ERP responded with success (e.g. 2xx).
- **✗ failure** — Send failed (network error, non-2xx response, or exception).
- **○ skipped** — Operation was not sent because `NODE_ENV !== "production"`.

Use this in local or staging to confirm the meter is being called and that the ERP is reachable. Leave `development` unset or `false` in production to avoid log noise.

```ts
const meter = createMeter({
  projectId: process.env.SOFT_WHERE_PROJECT_ID!,
  apiKey: process.env.SOFT_WHERE_API_KEY!,
  erpUrl: process.env.SOFT_WHERE_ERP_URL!,
  development: process.env.NODE_ENV !== "production",
});
```

---

## Next.js: use in `middleware.ts`

Middleware runs on the Edge runtime. You can use `@soft-where/meter` there to send request counts and duration to the SOFT-WHERE ERP. **Bandwidth:** in middleware, the response from `NextResponse.next()` usually has no `Content-Length` yet (the body is generated later), so bandwidth is reported as 0 unless you pass it explicitly (see below).

### Cursor agent instructions (Next.js middleware integration)

Use the following as both documentation and a prompt for a Cursor agent that should wire this package into a Next.js app’s middleware.

**Goal:** Integrate `@soft-where/meter` into the existing Next.js middleware so that every request is metered and reported to the SOFT-WHERE ERP. Do not break existing middleware logic (auth, redirects, etc.); add metering alongside it.

**Steps:**

1. **Install the package**  
   - Add dependency: `@soft-where/meter`.

2. **Environment variables**  
   - Ensure the Next.js project has these **server/edge-only** env vars (e.g. in `.env.local` or in the hosting provider’s dashboard). Do **not** prefix them with `NEXT_PUBLIC_`:
     - `SOFT_WHERE_PROJECT_ID` — SOFT-WHERE project ID.
     - `SOFT_WHERE_API_KEY` — Secret API key for the ERP.
     - `SOFT_WHERE_ERP_URL` — Full ERP ingest URL (e.g. `https://erp.soft-where.com/ingest`).
   - Document these in the project’s env example (e.g. `.env.example`) with short comments.

3. **Create a shared meter instance**  
   - In the same file as the middleware (or in a small module it imports), create the meter once using the env vars above:
     - `createMeter({ projectId, apiKey, erpUrl, environment?, development? })`.
     - Read `projectId`, `apiKey`, and `erpUrl` from `process.env.SOFT_WHERE_PROJECT_ID`, `process.env.SOFT_WHERE_API_KEY`, and `process.env.SOFT_WHERE_ERP_URL`.
     - Optionally set `development: process.env.NODE_ENV !== "production"` to get verbose success/failure logging in the terminal during development.
     - Only create/call the meter when these env vars are defined so the app does not crash in dev or when the integration is disabled.

4. **Wire metering into `middleware.ts`**  
   - In the middleware function:
     - Record the start time at the very beginning (e.g. `const start = Date.now()`).
     - Run the existing middleware logic and obtain the `NextResponse` (or other response) you intend to return.
     - Before returning, call `meter.trackRequest(request, response, durationMs)` or `meter.trackRequest(request, response, durationMs, bandwidthBytes)` where:
       - `request` is the middleware’s `NextRequest` (it is a standard `Request`).
       - `response` is the `NextResponse` (or response) you are about to return (it is a standard `Response`).
       - `durationMs` is `Date.now() - start`.
       - `bandwidthBytes` (optional): in middleware, the response often has no `Content-Length`, so bandwidth defaults to 0. Pass the actual response body size here if you have it (e.g. from a header you set elsewhere or from measuring the response). Otherwise omit it.
     - Return the same response as before so behavior is unchanged.
   - Do not `await` the meter; `trackRequest` is fire-and-forget and must not block the response.

5. **Edge compatibility**  
   - Middleware runs on the Edge runtime. `@soft-where/meter` is edge-compatible and uses the global `fetch` with `keepalive: true`. No extra config is required.

6. **Production-only behavior**  
   - The library only sends data when `NODE_ENV === "production"`. In development, all meter calls are no-ops; no need to guard calls in code.

**Summary for the agent:** Install `@soft-where/meter`, add env vars `SOFT_WHERE_PROJECT_ID`, `SOFT_WHERE_API_KEY`, and `SOFT_WHERE_ERP_URL` (no `NEXT_PUBLIC_`), create one meter with `createMeter()` from those env vars, then in `middleware.ts` measure duration and call `meter.trackRequest(request, response, durationMs)` before returning the response. Optionally pass a fourth argument `bandwidthBytes` if the app can provide response body size (otherwise bandwidth is 0 in middleware). Leave all other middleware logic intact.

---

### Minimal middleware example

```ts
// middleware.ts (Next.js)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createMeter } from "@soft-where/meter";

const projectId = process.env.SOFT_WHERE_PROJECT_ID;
const apiKey = process.env.SOFT_WHERE_API_KEY;
const erpUrl = process.env.SOFT_WHERE_ERP_URL;

const meter =
  projectId && apiKey && erpUrl
    ? createMeter({ projectId, apiKey, erpUrl })
    : null;

export function middleware(request: NextRequest) {
  const start = Date.now();
  const response = NextResponse.next();
  const durationMs = Date.now() - start;

  if (meter) {
    meter.trackRequest(request, response, durationMs);
  }

  return response;
}
```

## API reference

- **createMeter(options)**  
  `options.projectId` (string), `options.apiKey` (string), `options.erpUrl` (string), `options.environment?` (string), `options.development?` (boolean). When `development` is `true`, all meter operations are logged to the terminal (success, failure, and skipped).  
  Returns a **Meter** object.

- **meter.trackRequest(req, res, durationMs, bandwidthBytes?)**  
  `req` and `res` are the Web API `Request` and `Response` (or Next.js equivalents). `durationMs` is the request duration in milliseconds. Sends one request event. Bandwidth is taken from the optional `bandwidthBytes` argument if provided and ≥ 0; otherwise from `res.headers` `Content-Length` (often 0 in Next.js middleware). Pass `bandwidthBytes` when you know the response body size so the ERP gets accurate bandwidth.

- **meter.trackBlobUpload(bytes)** / **meter.trackBlobDelete(bytes)**  
  `bytes` is the size delta.

- **meter.trackCustomMetric(name, value)**  
  `name` (string), `value` (number).

- **meter.attemptDbSnapshot(getDbSize)**  
  `getDbSize` is a `() => Promise<number>` (e.g. Neon HTTP size). The library calls it and posts the result to the ERP without awaiting the network. Returns a Promise that resolves when the size has been obtained and the send has been triggered (or on error); never throws.

## License

MIT