Skip to main content
The Formo server-side SDKs allow you to track actions and identify users from your backend applications. Server-Side SDKs are stateless - it doesn’t store or remember any values between calls. Every call must explicitly provide wallet address values for identification. The Server-Side SDK is open source and implements the standard Events API.

Installation

Install the @formo/analytics-node package:
npm install @formo/analytics-node

Quick Start

Initialize the SDK with your project’s write key:
import { FormoAnalytics } from "@formo/analytics-node";

const analytics = new FormoAnalytics("<YOUR_WRITE_KEY>");

Identify users

Call identify() when a user signs in or connects their wallet to associate them with their actions.
import { v4 as uuid } from "uuid";

await analytics.identify({
  address: "0x9798d87366bdfc5d70b300abdffc4f9e95369b3d", // required: wallet address
  anonymousId: uuid(), // optional: auto-generated if not provided
  properties: {
    provider_name: "MetaMask",
    rdns: "io.metamask",
  },
});

Track events

To track custom events (actions, conversions, or backend states) use the track function.
await analytics.track({
  address: "0x9798d87366bdfc5d70b300abdffc4f9e95369b3d", // optional: wallet address
  anonymousId: uuid(), // optional: auto-generated if not provided
  event: "Purchase Completed", // required: event name
  properties: {
    order_id: "123",
    revenue: 99.99,
    currency: "USD",
  },
});
You can track volume, revenue, and points in custom events.

Configuration

Options

Customize the SDK behavior during initialization by passing an optional configuration object:
OptionTypeDefaultDescription
flushAtnumber20 eventsFlush the queue once it contains N events.
flushIntervalnumber30000 msFlush the queue every N milliseconds.
maxQueueSizenumber500000 bytesFlush when the queue exceeds N bytes (approx. 500KB).
retryCountnumber3 retriesNumber of times to retry failed requests.
const analytics = new FormoAnalytics("<YOUR_WRITE_KEY>", {
  flushAt: 20,
  flushInterval: 30000,
  maxQueueSize: 500000,
  retryCount: 3,
});

Manual flushing

To ensure all pending events are sent before a process exits (useful for serverless functions), call flush():
await analytics.flush();

Data validation

The SDK throws a ValidationError when required fields are missing or invalid:
import { FormoAnalytics, ValidationError } from "@formo/analytics-node";

try {
  await analytics.track({
    event: "", // empty event name will fail validation
  });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`Validation failed: ${error.field} - ${error.reason}`);
  }
}

Verification

To verify that your integration is working correctly:
  1. Send a test event: Trigger an action in your application that calls track() or identify().
  2. Check the Dashboard: Go to the Activity page in the Formo dashboard.
  3. Confirm Ingestion: Your events should appear in the activity stream within a few seconds after a flush occurs.
If you don’t see events, ensure that you are calling await analytics.flush() if your script exits immediately, and check for any ValidationError in your server logs.

Graceful shutdown

The SDK automatically attempts to flush pending events before the process closes by listening to the following Node.js process events:
  • beforeExit: Flushes events when the process is about to exit naturally.
  • SIGTERM: Flushes events when the process receives a termination signal (common in Docker/Kubernetes).
  • SIGINT: Flushes events when the process is interrupted (e.g., Ctrl+C).

Important caveats

  • Manual process.exit(): If your code calls process.exit() directly, the beforeExit event will not fire, and the queue will not be flushed.
  • Serverless Environments: In platforms like AWS Lambda or Vercel, the environment may be frozen immediately after your handler returns. You must call await analytics.flush() manually at the end of your function.
  • Forceful Shutdowns: A SIGKILL (kill -9) or sudden crash will prevent any automatic flushing.
For the most reliable delivery in mission-critical applications or serverless functions, we recommend calling await analytics.flush() explicitly:
process.on("SIGTERM", async () => {
  await analytics.flush();
  process.exit(0);
});