npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixels-online/pixels-analytics-node-sdk

v1.95.0

Published

Pixels Analytics Node.js SDK

Downloads

1,571

Readme

Pixels Analytics Node.js SDK

A TypeScript SDK for batching and sending analytics events to the Pixels event tracking API, with built-in retry, batching, and graceful shutdown support.

Features

  • Batching: Events are queued and sent in batches (default: 100, configurable).
  • Automatic Flushing: Events are sent every 5 seconds or when the batch size is reached.
  • Exponential Backoff: Retries failed requests with exponential backoff (default: 3 retries).
  • Graceful Shutdown: Flushes the queue on process exit or termination signals.
  • Strong Typing: TypeScript types for all supported event payloads.
  • Supports Test and Live Environments.
  • Native fetch: Requires Node.js 18+ (uses global fetch, no axios dependency).

Installation

npm install @pixels-online/pixels-analytics-node-sdk

Usage

import PixelsAnalytics from '@pixels-online/pixels-analytics-node-sdk';

const analytics = new PixelsAnalytics({
  apiKey: 'YOUR_API_KEY',
  clientId: 'YOUR_CLIENT_ID',
  env: 'test', // or 'live'
});

// Track a sign-in event
analytics.signIn('playerId123', {
  platform: 'email',
  platform_identifier: '[email protected]',
});

// Track a custom event (snake_case event name is enforced)
analytics.custom('custom_event', 'playerId123', { foo: 'bar' });

Event Tracking Methods

  • gainAchievement(playerId, payload, timestamp?)
  • loseAchievement(playerId, payload, timestamp?)
  • referUser(payload, timestamp?)
  • addTags(payload, timestamp?)
  • removeTags(payload, timestamp?)
  • signIn(playerId, payload, timestamp?)
  • signUp(playerId, payload, timestamp?)
  • trustScore(playerId, payload, timestamp?)
  • identifierLink(playerId, payload, timestamp?)
  • gainMembership(playerId, payload, timestamp?)
  • renewMembership(playerId, payload, timestamp?)
  • loseMembership(playerId, payload, timestamp?)
  • spendCurrency(playerId, payload, timestamp?)
  • earnCurrency(playerId, payload, timestamp?)
  • withdrawCurrency(playerId, payload, timestamp?)
  • depositCurrency(playerId, payload, timestamp?)
  • gainItem(playerId, payload, timestamp?)
  • gainManyItems(playerId, payload, timestamp?)
  • loseItem(playerId, payload, timestamp?)
  • loseManyItems(playerId, payload, timestamp?)
  • playerToPlayerTrade(payload, timestamp?)
  • funnelStart(playerId, payload, timestamp?)
  • funnelProgression(playerId, payload, timestamp?)
  • funnelEnd(playerId, payload, timestamp?)
  • questStart(playerId, payload, timestamp?)
  • questProgression(playerId, payload, timestamp?)
  • questEnd(playerId, payload, timestamp?)
  • levelUp(playerId, payload, timestamp?)
  • custom(eventName, playerId, payload, timestamp?) — Track a custom event (eventName is always converted to lower snake_case)

Offer Methods

fetchPlayerCampaigns(playerId, options?) This method refreshes a player's offers and then returns their current list of in-game offers.

claimRewards({kind: 'offer', instanceId, playerId}) This method allows you to claim an offer for a given player. By calling this method, it will mark the offer as "claimed" and then it will return an array of rewards which you will need to handle in your server.

See src/types.ts for detailed payload structures and type definitions.

Gift Card Methods (Bitrefill)

Redeem a player's in-game Stacked balance for a real gift card, emailed to them by Bitrefill. Every call acts on behalf of one player and authenticates with a short-lived game JWT minted from your API key — the server derives gameId and playerId from the token, never from the body.

Funding is scoped to your game. The card is paid for with that player's balance in your game only, never the cross-game aggregate on their Stacked account. getGiftCardHistory likewise reports only the redemptions your game funded.

Availability is gated per org: Stacked must enable gift cards (allowInAppGiftCardPurchases) for your org, and every gift-card call rejects with in-app-gift-cards-not-enabled until it does.

import PixelsAnalytics, {
  PixelsApiError,
  giftCardValueIssue,
} from '@pixels-online/pixels-analytics-node-sdk';

// 1. Is the feature available, and what can this player spend?
const config = await analytics.getGiftCardConfig(playerId);
if (!config) return; // gift cards are off platform-wide

// config.currencies[].balance is the player's balance IN YOUR GAME
const pixel = config.currencies.find((c) => c.id === 'cur_pixel');
if (!pixel || pixel.balance <= 0) return;

// The smallest card your game may hand out (USD; your org's own minimum or the
// platform default). config.min already includes it.
console.log(`cards start at $${config.minRedeemUsd}`);

// Per-person maximums (USD card value per rolling day / year, counting every
// card made out to this player — bought or minted) and what they have left.
// Offer at most config.remainingPerUserTodayUsd; past it the server rejects
// with 'gift-card-user-daily-limit-exceeded' / '-yearly-limit-exceeded'.
if (config.remainingPerUserTodayUsd <= 0) return;

// 2. Validate the card value BEFORE spending anything.
// min/max/step constrain the NET value (what the player receives after the
// surcharge), not the gross their balance converts to — see the helpers below.
const issue = giftCardValueIssue(config, 25);
if (issue) return; // 'usd-amount-too-small' | '-too-large' | '-not-on-step'

// 3. Price it without committing (persists nothing, moves no balance)
const quote = await analytics.quoteGiftCard(playerId, {
  sourceCurrencyId: 'cur_pixel',
  amount: 5000,
});
// quote.targetAmountAfterFee = card value the player receives
// quote.priceUsd = gross; quote.totalFee = surcharge between them
if (!quote.canWithdraw) return; // withdrawal limits currently block this

// 4. Buy — recipientEmail is REQUIRED, Bitrefill mails the code there
const result = await analytics.purchaseGiftCard(playerId, {
  sourceCurrencyId: 'cur_pixel',
  amount: 5000,
  recipientEmail: '[email protected]',
  recipientName: 'Player One', // optional
});
// { pendingWithdrawId, status: 'waiting', ... }

// 5. Poll for redemption status — code/link appear once complete
const { payouts } = await analytics.getGiftCardHistory(playerId, { limit: 25 });
for (const p of payouts) {
  if (p.status === 'complete') console.log(p.bitrefill?.code, p.bitrefill?.link);
  else if (p.status === 'rejected') console.log('refunded:', p.error);
}

Bounds and fee math

config.min / max / step constrain the net card value — what the player receives after the surcharge (feeFixed + feePercentage) — not the gross their balance converts to. Conflating the two is the easiest thing to get wrong here, so the SDK ships the same arithmetic the server runs (pure, no network):

import {
  giftCardValueIssue,   // null | 'usd-amount-too-small' | '-too-large' | '-not-on-step'
  giftCardNetValue,     // gross USD -> net card value after the surcharge
  giftCardGrossForNet,  // net card value -> gross USD needed (rounds up)
  snapGiftCardValue,    // snap DOWN to the nearest purchasable step
} from '@pixels-online/pixels-analytics-node-sdk';

giftCardValueIssue(config, 25);   // is a $25 card purchasable?
giftCardGrossForNet(config, 25);  // USD the player must spend to get one
snapGiftCardValue(config, 27);    // -> 20 on a $10-step product
giftCardNetValue(config, 25);     // what $25 of spend actually becomes

giftCardValueIssue returns the same codes the server rejects with, so one error → copy map covers both the pre-check and a real purchase failure.

purchaseGiftCard is not idempotent: each call is a separate purchase, so guard against double submission. A rejected redemption refunds the balance automatically; a failure to create one never debits.

Non-2xx responses throw PixelsApiError carrying the server's own code (insufficient-balance, usd-amount-not-on-step, no-stacked-account, in-app-gift-cards-not-enabled, …) — see src/errors.ts for the full list worth handling.

Minting gift cards (paid for by your org)

When the player pays you, in your own in-game currency, your server can mint the card instead of spending their Stacked balance. No Stacked balance is read or touched: your org is billed for the card, and only once Bitrefill delivers it. These two calls authenticate with your API key directly, never a player JWT, so a player's client can't mint cards on your bill.

// 1. Persist your own purchase id FIRST. It is the idempotency key.
const txId = 'order-8f14e45f';

// 2. Charge the player in your own currency (your code), then mint.
const card = await analytics.mintGiftCard(playerId, {
  txId,
  amountUsd: 10, // NET card value the player receives, in whole cents
  recipientEmail: '[email protected]', // optional: Bitrefill also mails the code
  externalCurrencies: [{ id: 'gold', amount: 500, fee: 25 }], // optional: what you charged
});
// { status: 'waiting', targetAmountAfterFee: 10, totalFee, priceUsd, duplicate: false, ... }

// 3. Poll for the outcome. The card also appears in getGiftCardHistory, with `txId`.
const latest = await analytics.getMintedGiftCard(txId); // null if never minted
if (latest?.status === 'complete') console.log(latest.bitrefill?.code);
else if (latest?.status === 'rejected') console.log('failed:', latest.error);
  • Idempotent on txId. A retry with the same txId returns the original card (duplicate: true) instead of buying a second one, so always retry a timed-out mint with the same id. Reusing a txId for a different player or amount is rejected with tx-id-conflict.
  • Billing. Your org is charged priceUsd: the card (targetAmountAfterFee) plus the gift-card surcharge (totalFee), which is added on top rather than taken out of the card. The charge is made once, when the card is delivered. Cards that fail are not billed.
  • No refunds. A card that fails ends rejected with error set. Nothing was debited on Stacked's side, so nothing is refunded; whether to return the player's in-game currency is up to you.
  • Cards pass through Stacked's approval queue like any payout, so status can sit at waiting for a while (autoApprovalEta says until when).
  • amountUsd must fit the product's bounds: getGiftCardConfig's min/max/step apply to it directly, since it is the net value.
  • externalCurrencies (optional) records what you charged the player in your own currencies: up to 20 { id, amount, fee } entries. It is stored on the card as reported and comes back from getMintedGiftCard and getGiftCardHistory, as an audit trail. Stacked never converts it or checks it against the card's value. Pass [] for "charged nothing"; a replay keeps the record from the first call.

Configuration Options

Pass an options object to the PixelsAnalytics constructor:

{
  apiKey: string;
  clientId: string;
  env: 'test' | 'live';
  maxBatchSize?: number; // default 100
  flushInterval?: number; // ms, default 5000
  maxRetries?: number; // default 3
}

Graceful Shutdown

The SDK automatically flushes the event queue on SIGINT, SIGTERM, or process exit.

Requirements

  • Node.js 18+ (for native fetch support)
  • TypeScript (for type safety, optional for JS usage)

Migration Notes

  • v2: Uses native fetch, not axios. No axios dependency required.
  • Event name normalization: The generic/custom method always converts event names to lower snake_case.

License

AGPLv3