@metricflow-ai/node
v0.1.2
Published
Server-side Node.js SDK for MetricFlow analytics.
Downloads
549
Readme
@metricflow-ai/node
Server-side Node.js SDK for MetricFlow analytics.
Table of Contents
- Installation
- Requirements
- Quick Start
- Identity Resolution
- Configuration
- Core API
- User Profiles (
people) - Revenue Tracking
- Error Handling
- License
Installation
npm install @metricflow-ai/nodeRequirements
- Node.js >= 18.17
- A MetricFlow
clientId+clientSecret(server-side credential — never exposeclientSecretto a browser)
Quick Start
const { MetricFlow } = require('@metricflow-ai/node');
const metricflow = new MetricFlow({
clientId: process.env.METRICFLOW_CLIENT_ID,
clientSecret: process.env.METRICFLOW_CLIENT_SECRET,
});
await metricflow.track('payment_succeeded', {
distinct_id: 'user_123',
amount: 99,
});
await metricflow.identify('user_123', undefined, { plan: 'pro' });
await metricflow.revenue.paymentSucceeded({
userId: 'user_123',
eventId: 'stripe_evt_123',
amount: 99,
currency: 'USD',
provider: 'stripe',
});
await metricflow.people.set('user_123', { plan: 'pro' });
await metricflow.flush();Every method also accepts a Node-style (error, result) callback as the last argument instead of using the returned Promise — pick whichever style fits your codebase.
Backend SDKs are stateless by design. Pass distinct_id, user_id, or
anonymous_id on each event. For request-linked backend events, pass request
context so MetricFlow can enrich IP, browser, OS, and device from the original
visitor request.
Identity Resolution
Identity is resolved in this priority order, from the options argument or top-level properties:
userId(orproperties.user_id) — the strongest identifier.distinctId(orproperties.distinct_id) — used asuser_idonly ifuserIdisn't set.anonymousId(orproperties.anonymous_id) — for events with no known user.
At least one of these is required, or the call throws.
Linking a backend event to a user from your frontend SDK
To attribute a backend event (e.g. a Stripe webhook firing revenue.paymentSucceeded) to the
same person already tracked by your browser or React Native app, pass the exact same userId
that SDK used in its own identify() call:
// Browser or React Native app, at login:
// metricflow.identify("user_123", { email: "...", plan: "free" });
// Backend, later — same user_id, different SDK:
await metricflow.revenue.paymentSucceeded({
userId: 'user_123', // must match the frontend identify() call exactly
eventId: 'stripe_evt_123',
amount: 99,
currency: 'USD',
provider: 'stripe',
});Both events resolve to the same user profile automatically — no extra field, session ID, or setup
is needed. userId values are compared exactly (case-insensitive only when the value looks like an
email); IDs like usr_abc123 must match byte-for-byte across every SDK sending them.
Configuration
const metricflow = new MetricFlow({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
batchSize: 50,
flushIntervalMs: 5000,
requestTimeoutMs: 90000,
maxRetries: 3,
maxQueueSize: 10000,
autoFlush: true,
debug: false,
});| Field | Type | Required? | Description |
| --- | --- | --- | --- |
| clientId | string | Required | Your project's client ID. |
| clientSecret | string | Required | Your project's server-side secret. Never expose this in browser code. |
| apiHost | string | Optional | Base API host. Only needed to point at a custom deployment — most integrations don't need this. |
| endpoint | string | Optional | Override the full track endpoint URL directly instead of deriving it from apiHost. |
| batchSize | number | Optional | Events per batch flush. Default: 50. |
| flushIntervalMs | number | Optional | Auto-flush interval in milliseconds. Default: 5000. |
| requestTimeoutMs | number | Optional | HTTP request timeout. Default: 90000. |
| maxRetries | number | Optional | Retry attempts on transient failures. Default: 3. |
| maxQueueSize | number | Optional | Max events buffered before oldest events are dropped. Default: 10000. |
| autoFlush | boolean | Optional | Automatically flush on the flushIntervalMs timer. Default: true. |
| debug | boolean | Optional | Verbose logging to the console. Default: false. |
Core API
track(eventName, properties, options?)
Queue a custom event. Batched and flushed automatically (or manually via flush()).
await metricflow.track('workspace_created', {
distinct_id: 'user_123',
workspace_name: 'Acme Inc',
});options (all optional): distinctId, userId, anonymousId, timestamp, insertId, idempotencyKey, context.
track_batch(events)
Track multiple events in one call — each item is { event, properties?, distinct_id?, user_id?, anonymous_id?, timestamp?, insert_id?, idempotency_key?, context? }.
await metricflow.track_batch([
{ event: 'signup_started', distinct_id: 'user_123' },
{ event: 'signup_completed', distinct_id: 'user_123' },
]);identify(userId, anonymousId?, traits?)
Link a known user to their profile traits.
await metricflow.identify('user_123', undefined, {
name: 'Ananya Rao',
email: '[email protected]',
plan: 'pro',
});alias(newId, originalId)
Merge two identities (e.g. anonymous ID → logged-in user ID).
await metricflow.alias('user_123', 'anon_abc');Flush / Close
await metricflow.flush(); // send whatever's queued right now
await metricflow.close(); // flush + stop the auto-flush timer (call on process shutdown)User Profiles (people)
Set and update persistent user profile properties.
await metricflow.people.set('user_123', { plan: 'pro', company: 'Acme Inc' });
await metricflow.people.set_once('user_123', { first_seen: new Date().toISOString() });
await metricflow.people.increment('user_123', 'login_count', 1);
await metricflow.people.append('user_123', 'tags', 'beta_user');
await metricflow.people.union('user_123', 'roles', ['admin']);
await metricflow.people.unset('user_123', ['temp_flag']);
// Revenue-style running totals on the profile
await metricflow.people.track_charge('user_123', 49, { plan: 'pro' });
await metricflow.people.clear_charges('user_123');| Method | Description |
| --- | --- |
| set(distinctId, properties) | Overwrite profile properties. |
| set_once(distinctId, properties) | Set only if the property doesn't already exist. |
| increment(distinctId, property, value?) | Increment a numeric property (default +1). |
| append(distinctId, property, value) | Append a value to a list property. |
| union(distinctId, property, value) | Add values to a list property without duplicates. |
| unset(distinctId, properties[]) | Remove properties from the profile. |
| track_charge(distinctId, amount, properties?) | Append a transaction record. |
| clear_charges(distinctId) | Clear all transaction records. |
Revenue Tracking
Revenue is server-authenticated only — always initialise with a
clientSecret. Revenue sent from a browser/web context is rejected by the
backend, so a malicious client can't inflate your numbers.
There are two ways to record revenue:
1. Custom events — add a $revenue property
Mark any event as revenue by attaching a $revenue amount and a currency.
This is the easiest path for in-app purchases with your own event names.
await metricflow.track('order_completed', {
distinct_id: 'user_123',
$revenue: 24.99, // the amount — negative = refund
currency: 'USD', // any 3-letter ISO code (USD, EUR, INR, …)
insert_id: 'order_789', // unique id → dedup (required)
plan: 'pro', // any extra business props are fine
});
// A refund — negative $revenue
await metricflow.track('order_refunded', {
distinct_id: 'user_123',
$revenue: -24.99,
currency: 'USD',
insert_id: 'refund_789',
});$revenue is the only $-prefixed property the backend accepts; all other
$…/__… keys are rejected.
2. Named billing events — revenue.* helpers
For payment-provider webhooks (Stripe / Apple / Google) with canonical names,
use the typed helpers. The amount comes from amount; refundCreated is stored
as negative revenue automatically.
await metricflow.revenue.paymentSucceeded({
userId: 'user_123',
eventId: 'stripe_evt_123', // → insert_id + idempotency_key
amount: 99,
currency: 'USD',
provider: 'stripe',
});Helpers: paymentSucceeded, invoicePaid, refundCreated, subscriptionCreated,
subscriptionUpdated, subscriptionCancelled.
Required fields (both paths)
| Field | Notes |
|-------|-------|
| identity | distinct_id / userId — required |
| amount | $revenue (non-zero) or amount (> 0) |
| currency | 3-letter ISO code, case-insensitive |
| dedup id | insert_id / idempotency_key (helpers derive it from eventId) |
Missing any of these → the event is rejected and no revenue is recorded.
Revenue is aggregated downstream on the computed revenue property.
Error Handling
All API calls reject with a subclass of MetricFlowError:
const { MetricFlowConfigError, MetricFlowHttpError } = require('@metricflow-ai/node');
try {
await metricflow.track('event_name', { distinct_id: 'user_123' });
} catch (err) {
if (err instanceof MetricFlowHttpError) {
console.error('HTTP failure:', err.status, err.body);
} else if (err instanceof MetricFlowConfigError) {
console.error('Bad config:', err.message);
} else {
throw err;
}
}| Error | Thrown when |
| --- | --- |
| MetricFlowConfigError | Invalid constructor options (missing clientId/clientSecret, bad URL, non-positive numeric option). |
| MetricFlowHttpError | The ingestion request failed. Has .status, .body, and .retryAfterMs. |
| MetricFlowError | Base class for both — catch this to handle any SDK error generically. |
Failed batches are retried automatically up to maxRetries times before the error is raised.
License
MIT — see LICENSE.
