Node Core SDK Migration Guide
Migrate from the legacy Node JS Server SDK to the new Node JS Core SDK
Why migrate?
- Performance: Node JS Core evaluates faster than the legacy SDK.
- New Features: Access to Parameter Stores, CMAB (Contextual Multi-Armed Bandits), observabilityClient, and more.
- Future Support: All new features and improvements are only available in Node JS Core.
- Maintenance: The legacy Node JS SDK is in maintenance mode and only receives critical bug fixes.
Installation
npm install @statsig/statsig-node-core
StatsigUser
In the Node Core SDK, StatsigUser is now a class instead of a TypeScript type interface. This is a key architectural change that affects how you create user objects.
Type definitions
// StatsigUser is a class with a constructor
class StatsigUser {
constructor(
args: ({ userID: string } | { customIDs: Record<string, string> }) &
StatsigUserArgs,
);
// Static factory methods
static withUserID(userId: string): StatsigUser;
static withCustomIDs(customIds: Record<string, string>): StatsigUser;
// Properties with getters/setters
userID: string | null;
customIDs: Record<string, string> | null;
email: string | null;
ip: string | null;
userAgent: string | null;
country: string | null;
locale: string | null;
appVersion: string | null;
custom: Record<
string,
string | number | boolean | Array<string | number | boolean>
> | null;
privateAttributes: Record<
string,
string | number | boolean | Array<string | number | boolean>
> | null;
statsigEnvironment: {
tier?: string;
[key: string]: string | undefined;
} | null;
toJSON(): string;
}
Creating users
import { StatsigUser } from "@statsig/statsig-node-core";
// Option 1: Using constructor with object
const user = new StatsigUser({
userID: "user_123",
email: "user@example.com",
ip: "192.168.0.1",
userAgent: "Mozilla/5.0",
country: "US",
custom: {
subscription_level: "premium",
},
});
// Option 2: Using static factory method
const userById = StatsigUser.withUserID("user_123");
// Option 3: Using customIDs
const userByCustomId = StatsigUser.withCustomIDs({ companyID: "company_456" });
Key Difference: In Node Core, you must use new StatsigUser({...}) to create a user object. Plain objects won't work and cause type errors.
API changes
Key package and class changes
| Feature | Node Core SDK | Legacy Node SDK | Status |
|---|---|---|---|
| Package | @statsig/statsig-node-core | statsig-node | ⚠️ Renamed |
| Options | StatsigOptions | StatsigOptions | ✓ Same |
| User | StatsigUser | StatsigUser | ⚠️ Changed from type to class |
| Initialize | await statsig.initialize() | await statsig.initialize() | ✓ Same |
| Check Gate | statsig.checkGate() | statsig.checkGate() | ✓ Same |
| Get Config | statsig.getDynamicConfig() | statsig.getConfig() | ⚠️ Renamed |
| Get Experiment | statsig.getExperiment() | statsig.getExperiment() | ✓ Same |
| Get Layer | statsig.getLayer() | statsig.getLayer() | ✓ Same |
| Log Event | statsig.logEvent() | statsig.logEvent() | ✓ Same |
| Shutdown | await statsig.shutdown() | await statsig.shutdown() | ✓ Same |
Singleton pattern changes
The Node Core SDK provides a different approach to singleton management compared to the legacy SDK.
import { Statsig, StatsigUser } from "@statsig/statsig-node-core";
// Option 1: Instance-based (recommended for most use cases)
const statsig = new Statsig("secret-key");
await statsig.initialize();
// Option 2: Shared singleton pattern
Statsig.newShared("secret-key", options); // Creates the shared instance
await Statsig.shared().initialize(); // Access via Statsig.shared()
// Check if shared instance exists
if (Statsig.hasShared()) {
const result = Statsig.shared().checkGate(user, "my_gate");
}
// Remove shared instance when done
Statsig.removeSharedInstance();
Key Difference: The legacy SDK uses a single global singleton that you access directly using Statsig.methodName(). The Node Core SDK supports both instance-based usage (new Statsig()) and an explicit shared singleton pattern (Statsig.newShared() / Statsig.shared()). The instance-based approach is recommended because it provides better control over SDK lifecycle.
Config Value Access with get() Method
Starting in version 0.10.0, the Node Core SDK supports the get() method on DynamicConfig, Experiment, and Layer objects, matching the legacy SDK's API.
import { Statsig, StatsigUser } from "@statsig/statsig-node-core";
const statsig = new Statsig("secret-key");
await statsig.initialize();
const user = new StatsigUser({ userID: "user_123" });
// Get a dynamic config
const config = statsig.getDynamicConfig(user, "my_config");
// Access values using get() with type-safe fallback (available in 0.10.0+)
const stringValue = config.get("param_name", "default_string");
const numberValue = config.get("count", 0);
const boolValue = config.get("enabled", false);
// Alternative: getValue() also available
const value = config.getValue("param_name", "default");
// Same pattern works for experiments
const experiment = statsig.getExperiment(user, "my_experiment");
const variant = experiment.get("variant", "control");
// And layers
const layer = statsig.getLayer(user, "my_layer");
const layerValue = layer.get("param", "default");
The get<T>(paramName, fallback) method provides type inference based on the fallback value type, making it easier to work with typed configuration values.
Complete sample code
The following are complete, working examples showing the same functionality in both the legacy Node SDK and the Node Core SDK.
import {
Statsig,
StatsigUser,
StatsigOptions,
} from "@statsig/statsig-node-core";
async function main() {
// 1. Configure options
const options: StatsigOptions = {
environment: "development",
specsSyncIntervalMs: 60000,
eventLoggingFlushIntervalMs: 10000,
};
// 2. Create SDK instance (instance-based approach)
const statsig = new Statsig("secret-your-server-key", options);
// 3. Initialize the SDK
await statsig.initialize();
// 4. Create a user (must use class constructor)
const user = new StatsigUser({
userID: "user_123",
email: "user@example.com",
custom: {
subscription_tier: "premium",
signup_date: "2024-01-15",
},
});
// 5. Check a feature gate
const showNewFeature = statsig.checkGate(user, "new_feature_gate");
console.log("Show new feature:", showNewFeature);
// 6. Get a dynamic config (note: getDynamicConfig, not getConfig)
const config = statsig.getDynamicConfig(user, "app_config");
const welcomeMessage = config.get("welcome_message", "Hello!");
const maxItems = config.get("max_items", 10);
console.log("Welcome message:", welcomeMessage);
console.log("Max items:", maxItems);
// 7. Get an experiment
const experiment = statsig.getExperiment(user, "checkout_flow_test");
const variant = experiment.get("variant", "control");
const buttonColor = experiment.get("button_color", "#007bff");
console.log("Experiment variant:", variant);
console.log("Button color:", buttonColor);
// 8. Get a layer
const layer = statsig.getLayer(user, "homepage_layer");
const heroText = layer.get("hero_text", "Welcome to our app");
console.log("Hero text:", heroText);
// 9. Log a custom event
statsig.logEvent(user, "purchase_completed", 49.99, {
item_id: "SKU_12345",
currency: "USD",
});
// 10. Shutdown gracefully
await statsig.shutdown();
console.log("Statsig shutdown complete");
}
main().catch(console.error);
Behavioral changes
StatsigOptions changes
The table below shows the mapping between legacy SDK options and Server Core SDK options:
| Old Option | New / Notes |
|---|---|
api | Deprecated |
idlists_thread_limit, logging_interval, enable_debug_logs, events_flushed_callback | Deprecated |
timeout | Deprecated (was only for network) |
apiForDownloadConfigSpecs | specsUrl – must complete with endpoint |
apiForIdLists | idListsUrl – must complete with endpoint |
apiForLogEvent | logEventUrl – must complete with endpoint |
initTimeoutMs | initTimeoutMs – applies to overall initialization (suggest adding +1000 ms if enabling UA) |
rulesetsSyncIntervalMs | specsSyncIntervalMs (unit: milliseconds) |
idListsSyncIntervalMs | idListsSyncIntervalMs (unit: milliseconds) |
loggingIntervalMs | eventLoggingFlushIntervalMs |
loggingMaxBufferSize | eventLoggingMaxQueuesize |
dataAdapter | Still supported but interface changed |
observability_client, sdk_error_callback | Now within observability_client interface |
logger | Now OutputLoggerProvider interface |
bootstrap_values, evaluation_callback, rules_updated_callback | Not yet supported |
Recommended migration path
Add the new Dependencies
- Add the new SDK package/module and any required platform-specific dependencies for your environment.
- Update import or require statements to reference the new SDK namespace or module.
- Keep the old package in place during local testing; having both running in parallel briefly can help. Alias the new package if needed.
Update Initialization
- Switch to the new initialization syntax. New SDK keys are not required. If you update them, ensure they have the same target apps.
- Use the appropriate builder or configuration pattern for setting options, and migrate the StatsigOptions you use, because some were renamed.
Update User Creation
- Migrate to the new format for creating user objects.
- Use the builder pattern or updated constructor to set user properties if needed.
Update Method Calls
- Start by migrating a few calls, replacing
oldStatsig.getExperiment()withnewStatsig.getExperiment(). - Test your service locally or with existing test patterns to build confidence in the new SDK's operation.
- As you migrate additional calls, update method names that changed (notably,
get_config()toget_dynamic_config()). - After completing the initial migration, use refactoring tools to migrate remaining calls in bulk.
- Start by migrating a few calls, replacing
Test Thoroughly
- Verify that all of your configs are successfully migrated - run your test suites end-to-end.
Remove old SDK
- Remove references to the old SDK, and clean up old initialization and import logic.
Need help?
If you encounter any issues during migration, reach out:
Was this helpful?