ابرووا
شروع رایگان
☰ فهرست مستندات · Node.js
به‌زودی. Abrova A/B Testing به‌زودی فعال می‌شود. این صفحه توضیح می‌دهد چطور کار خواهد کرد؛ هنوز قابل استفاده نیست.
این صفحه هنوز ترجمه نشده — نسخه‌ی انگلیسی نمایش داده می‌شود. English

A/B Testing Node.js SDK

Server-side TypeScript SDK for A/B experiments, feature flags, and event tracking on Node.js backends. Per-request user handles with forUser(), server secret, user token minting, graceful shutdown.

Node.js 0.2.3Node.js 18+به‌روزرسانی: ۵ آبان ۱۴۰۵
AbrovaAbTesting Node.js SDK v0.2.3.

Server-side SDK for Node.js backends and APIs. One handle per request with forUser(), so users never see each other's variants. Proves it runs on your server with your project's server secret, mints user tokens for your apps with createUserToken(), and flushes pending events on graceful shutdown with destroy(). Zero dependencies.

Requirements.
  • Node.js 18 or newer (the SDK uses the built-in fetch).
  • TypeScript supported out of the box.
  • Your project's API key and server secret.
What to know.
  • forUser(userId, attributes) returns a handle that acts for one user. Use one per request on a server.
  • serverSecret in init(): required for anything that acts for a user (assigning variants, tracking events, evaluating flags). From 0.2.2, init() warns when it is missing.
  • createUserToken(userId, expiresIn?) mints the user token your browser and mobile apps need.
  • Variants are decided by the service only. When it can't be asked, getVariant returns the cached assignment or your default.
  • setUserId remains for scripts and single-user processes only.
Server-Side vs Client-Side.
  • In-memory storage: assignments are kept in memory per user and experiment, not in localStorage
  • Per-request users: forUser() handles carry their own user, so concurrent requests never overwrite each other
  • Server secret instead of user tokens: your server is trusted with the user ids it passes
  • destroy() method: flushes pending events before cleanup (use in graceful shutdown)

Installation#

bash
# 1. .npmrc in your project root: @abrova packages come from Abrova Center
@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
bash
# 2. Install
npm install @abrova/ab-testing-node@^0.2.3

Or with Yarn:

bash
yarn add @abrova/ab-testing-node@^0.2.3

Server secret and user tokens#

This SDK runs on your server, so it proves that with your project's server secret (serverSecret in init). The service then trusts the user ids you pass to forUser(). Listing experiments and flags works with the API key alone, but assigning variants, tracking events and evaluating flags are refused without the secret. init() prints a warning when serverSecret is missing; variants and flags then come back as your defaults, events stay in the queue (at most 1,000), and the refusal is reported once through the error event.

typescript
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

await AbrovaAbTesting.init({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET, // from Abrova Console; server only
});
Keep the server secret on the server.

Never put the server secret in an app, a browser bundle, a NEXT_PUBLIC_ variable, or a public repo. Load it from your server's environment or secret store.

Mint user tokens for your apps

Your browser and mobile apps can't hold the secret. They send a short-lived user token that your server mints for the signed-in user. createUserToken does that for you:

typescript
app.post('/abrova-token', requireLogin, async (req, res) => {
  // expiresIn is optional: seconds, 3600 by default, 86400 max
  res.json(await AbrovaAbTesting.createUserToken(req.session.userId, 3600));
});
// → { "token": "eyJhbGciOi…", "userId": "user-123", "expiresIn": 3600, ... }

It calls the Abrova auth endpoint with your API key and server secret. From any other backend language, make the same call directly:

bash
curl -X POST https://auth.abrova.ir/users/token \
  -H "x-api-key: $ABROVA_API_KEY" \
  -H "x-server-secret: $ABROVA_SERVER_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"userId": "user-123", "expiresIn": 3600}'

# → { "token": "eyJhbGciOi…", "userId": "user-123", "expiresIn": 3600, "expiresAt": "…" }

Only mint a token for the user who is signed in on that request. Your app's SDK gets it through its tokenProvider, which calls this endpoint.

One token for A/B Testing, Chat and Sync

The token is the same Abrova user token used by Abrova Chat and Abrova Sync. If your app uses them too, mint one token from this endpoint and give it to every SDK's tokenProvider instead of minting one per product.

Keep it on the device.

A token is valid until it expires, so your app should store it with the user's session and reuse it. The app SDKs cache the token and only call your endpoint again shortly before it expires, not on every request.

Initialization#

Initialize once, when your server starts. Use the default singleton or create your own instances.

Singleton (Simple)

typescript
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

await AbrovaAbTesting.init({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET,
  debug: process.env.NODE_ENV !== 'production',
  // flushInterval: 10000, // your own send interval in ms; leave it out to follow the console
});

Multiple Instances (Multi-Tenant)

typescript
import { AbrovaAbTestingSDK } from '@abrova/ab-testing-node';

// Create separate instances per project
const tenantA = new AbrovaAbTestingSDK();
await tenantA.init({ apiKey: 'ab_live_tenant_a_key', serverSecret: process.env.TENANT_A_SECRET });

const tenantB = new AbrovaAbTestingSDK();
await tenantB.init({ apiKey: 'ab_live_tenant_b_key', serverSecret: process.env.TENANT_B_SECRET });

const user = tenantA.forUser('user-123');

Send interval and list cache

Two values come from your project's settings in the Abrova console (A/B Testing › Settings › Defaults):

  • Send interval — how often queued events are sent. The SDK follows it when you leave flushInterval out. A flushInterval you pass is your own and is always used.
  • List cache — how long the SDK uses the experiment list before it asks the service again. There is no SDK option for it. Once the list is older than that, the SDK fetches it again in the background the next time your code reads a variant or tracks an event. The call doesn't wait for it, and a failed refresh does not emit an error event.

A change you make in the console (a new experiment, changed weights or traffic) reaches running servers within the list cache time, without a restart. A user who already has a variant keeps it. A changed send interval is picked up at that same refresh. The SDK keeps the console's values within safe limits, and sends every 30 seconds until it knows the console's value. Apps on SDK versions older than 0.2.3 keep their built-in behaviour.

From 0.2.3, a flushInterval set in code always wins.

User Management#

A server answers many users at once. Always go through forUser(userId, attributes): each handle carries its own user and attributes, and assignments are cached per user and experiment, so one user's variant is never served to another. Handles are cheap: create one per request.

typescript
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

const user = AbrovaAbTesting.forUser('user-123', {
  plan: 'premium',
  country: 'US',
  app_version: '2.0.0',
});

user.userId; // 'user-123'

const variant = await user.getVariant('checkout-flow', 'control');

Express.js Middleware Example

typescript
import express from 'express';
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

const app = express();

// Initialize once at startup
await AbrovaAbTesting.init({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET,
});

// Per-request handle
app.use((req, res, next) => {
  if (req.user) {
    res.locals.ab = AbrovaAbTesting.forUser(req.user.id, {
      plan: req.user.plan,
      country: req.headers['cf-ipcountry'] || 'unknown',
    });
  }
  next();
});

app.get('/checkout', async (req, res) => {
  const variant = await res.locals.ab.getVariant('checkout-flow', 'control');
  res.json({ variant });
});
setUserId is for single-user processes.

setUserId() / setUserAttributes() set the SDK's own user and still work for scripts, workers and CLIs that act for one user. On a server, two requests would overwrite each other's user, so don't use them there.

Getting Experiment Variants#

The examples below use user = AbrovaAbTesting.forUser(userId). getVariant returns the variant's name as it appears in Abrova Console (for example Control or Variant B), not its key (control, variant-b). The variant always comes from the service and is cached in memory per user and experiment; the SDK never works one out itself. If the service doesn't return an assignment (it can't be reached, or serverSecret is missing) and nothing is cached, you get your default.

typescript
// Get variant for this user
const variant = await user.getVariant(
  'checkout-button-color',
  'control'  // returned when the service refuses or cannot be reached and nothing is cached
);

// Use in your response
if (variant === 'Variant B') {
  return { layout: 'modern', color: '#22C55E' };
} else {
  return { layout: 'classic', color: '#6B7280' };
}

// Get variant configuration
const config = await user.getVariantConfig('checkout-button-color');

// Get all experiments (not user specific, synchronous)
const experiments = AbrovaAbTesting.getExperiments();

Tracking Events#

Track server-side events. All 17 event types are supported. Events are queued in memory and sent in batches. The queue holds at most 1,000 events, and the SDK waits before retrying when the service is busy or your project has reached its usage limit. Events the service refuses (for example, no serverSecret) stay in the queue and are reported once through the error event. Each event carries the time your server recorded it, so events sent together keep their order.

typescript
import { EventType } from '@abrova/ab-testing-node';

// Core events
await user.trackView('checkout-flow');
await user.trackClick('checkout-flow');
await user.trackConversion('checkout-flow', 1000000);
await user.trackCustomEvent('checkout-flow', 'api_call', {
  endpoint: '/api/checkout',
  duration_ms: 250,
});

// Conversion
await user.trackPurchase('checkout-flow', 1250000, 'txn-123', {
  currency: 'Toman',
});

// Auth events
await user.trackSignUp('onboarding-flow', 'google');
await user.trackLogin('onboarding-flow', 'email');

// Generic event
await user.trackEvent(
  'checkout-flow',
  EventType.CUSTOM,
  'page_load',
  1.5,
  { route: '/checkout' }
);

Feature Flags#

Evaluating a flag for a user goes through the user handle and needs the server secret. Listing flags is not user specific and works with the API key alone.

typescript
// Check if feature is enabled for this user
const isEnabled = await user.isFeatureEnabled(
  'new-checkout-flow',
  false  // default value
);

// Get feature value for this user
const maxRetries = await user.getFeatureValue(
  'max-retries',
  3  // default
);

// Get all flags
const flags = await AbrovaAbTesting.getFeatureFlags();

// Refresh flags from server (emits 'featureFlagsRefreshed')
await AbrovaAbTesting.refreshFeatureFlags();

Lifecycle & Graceful Shutdown#

Important for server applications: flush events before shutting down.

typescript
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

// Flush events immediately
await AbrovaAbTesting.flush();

// Graceful shutdown: flushes pending events, then cleans up
await AbrovaAbTesting.destroy();

// Use with process signals
process.on('SIGTERM', async () => {
  console.log('Shutting down...');
  await AbrovaAbTesting.destroy();
  process.exit(0);
});

process.on('SIGINT', async () => {
  await AbrovaAbTesting.destroy();
  process.exit(0);
});

Reset (without flush)

typescript
// Reset immediately: clears state without flushing
AbrovaAbTesting.reset();

Event Listeners#

typescript
const unsubscribe = AbrovaAbTesting.on('experimentAssigned', (event) => {
  const { variantKey } = event.data as { variantKey: string };
  console.log(`User assigned to ${variantKey}`);
});

AbrovaAbTesting.on('syncCompleted', (event) => {
  const { synced, pending } = event.data as { synced: number; pending: number };
  console.log(`Synced ${synced} events, ${pending} pending`);
});

AbrovaAbTesting.on('error', (event) => {
  console.error('AbrovaAbTesting error:', (event.data as { message: string }).message);
});

// Cleanup
unsubscribe();

Available events: initialized, error, experimentAssigned, experimentsRefreshed, featureFlagsRefreshed, syncCompleted.

Complete Express.js Example#

typescript
import express from 'express';
import { AbrovaAbTesting } from '@abrova/ab-testing-node';

const app = express();
app.use(express.json());

// Initialize SDK
await AbrovaAbTesting.init({
  apiKey: process.env.ABROVA_API_KEY!,
  serverSecret: process.env.ABROVA_SERVER_SECRET,
  debug: process.env.NODE_ENV !== 'production',
  flushInterval: 15000, // your own send interval; leave it out to follow the console
});

// One handle per request, for the signed-in user (your auth sets req.user)
app.use((req, res, next) => {
  if (!req.user) return res.sendStatus(401);
  res.locals.ab = AbrovaAbTesting.forUser(req.user.id, {
    plan: req.user.plan || 'free',
  });
  next();
});

// Mint an Abrova user token for your web and mobile apps
app.post('/api/abrova-token', async (req, res) => {
  res.json(await AbrovaAbTesting.createUserToken(req.user.id));
});

// Get variant for checkout experiment
app.get('/api/checkout-variant', async (req, res) => {
  const variant = await res.locals.ab.getVariant('checkout-flow', 'control');
  const config = await res.locals.ab.getVariantConfig('checkout-flow');
  res.json({ variant, config });
});

// Track purchase
app.post('/api/purchase', async (req, res) => {
  const { total, transactionId } = req.body;
  await res.locals.ab.trackPurchase('checkout-flow', total, transactionId);
  await res.locals.ab.trackConversion('checkout-flow', total);
  res.json({ success: true });
});

// Check feature flag
app.get('/api/feature/:key', async (req, res) => {
  const enabled = await res.locals.ab.isFeatureEnabled(req.params.key, false);
  const value = await res.locals.ab.getFeatureValue(req.params.key);
  res.json({ key: req.params.key, enabled, value });
});

// Graceful shutdown
const server = app.listen(3000, () => {
  console.log('Server running on :3000');
});

process.on('SIGTERM', async () => {
  await AbrovaAbTesting.destroy();
  server.close();
});

Best Practices#

One handle per request

Call forUser() with the request's user. Never share setUserId() across concurrent requests.

Secret stays on the server

Pass serverSecret from your environment. Apps get short-lived user tokens from createUserToken(), never the secret.

One token for every Abrova SDK

The token you mint works for A/B Testing, Chat and Sync. Your apps fetch it once and share it.

Flush on shutdown

Call destroy() on SIGTERM so queued events are sent before the process exits.

TypeScript Types#

typescript
import type {
  AbrovaAbTestingConfig,
  AbrovaAbTestingUser,
  UserToken,
  Experiment,
  Variant,
  Assignment,
  FeatureFlag,
  FlagVariant,
  AbrovaAbTestingEventType,
  AbrovaAbTestingEvent,
} from '@abrova/ab-testing-node';
import { EventType } from '@abrova/ab-testing-node';

interface AbrovaAbTestingConfig {
  apiKey: string;
  serverSecret?: string;          // your project's server secret (server only); required for variants, events, flags and createUserToken
  debug?: boolean;
  flushInterval?: number;         // ms between sends; leave it out to follow the console's Send interval
  maxQueueSize?: number;          // events kept while waiting to send (default and maximum: 1000)
  maxCachedAssignments?: number;  // (user, experiment) assignments kept in memory (default: 10000)
}

interface UserToken {
  token: string;
  userId: string;
  expiresIn: number;              // seconds until the token expires
}

// EventType enum
enum EventType {
  VIEW = 'view',
  CLICK = 'click',
  CONVERSION = 'conversion',
  CUSTOM = 'custom',
  SCROLL = 'scroll',
  FORM_SUBMIT = 'form_submit',
  SEARCH = 'search',
  SHARE = 'share',
  ADD_TO_CART = 'add_to_cart',
  REMOVE_FROM_CART = 'remove_from_cart',
  BEGIN_CHECKOUT = 'begin_checkout',
  PURCHASE = 'purchase',
  VIDEO_START = 'video_start',
  VIDEO_COMPLETE = 'video_complete',
  SIGN_UP = 'sign_up',
  LOGIN = 'login',
  LOGOUT = 'logout',
}
این صفحه مفید بود؟
گزارش مشکل