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

@cherrydotfun/collector-sdk

v0.3.0

Published

TypeScript SDK for Cherry Collector — event tracking, logging, and metrics

Readme

@cherrydotfun/collector-sdk

TypeScript SDK for Cherry Collector — event tracking, logging, and metrics.

Collect errors, logs, and analytics events from your application and stream them to the Collector backend. Zero dependencies, works on browser, React Native, and Node.js.

Installation

npm install @cherrydotfun/collector-sdk
yarn add @cherrydotfun/collector-sdk
bun add @cherrydotfun/collector-sdk

Quick Start

Initialize the client, send events, and flush before exit:

import { CollectorClient, SolanaTokens } from '@cherrydotfun/collector-sdk';

const collector = new CollectorClient({
  baseUrl: 'https://analytics.cherry.fun',
  apiKey: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  platform: 'ios',
  version: '2.1.0',
});

// Initialize: fetch filter config and start refresh timer
await collector.init();

// Set context (attached to all subsequent events)
collector.setUser('wallet_address_or_user_id');
collector.setSession('session_uuid');
collector.setModule('messaging');
collector.setDevice('device_id');

// Send events
collector.error('WebSocket connection failed', { url: '/ws' });
collector.info('User logged in', { userId: 'wallet123' });
collector.debug('Cache hit', { key: 'messages_dm_123' });
collector.analytics('screen_view', 'ChatScreen', { fromScreen: 'RoomList' });
collector.analytics('button_click', 'Send message', { hasAttachment: true });

// Record revenue (immediate, not batched — await it). amount is human-readable.
await collector.revenue({ source: 'swap', amount: 0.005, token: SolanaTokens.WSOL });

// Set metrics
await collector.metric('ws_connections', 42);
await collector.metricBatch([
  { key: 'ws_connections', value: 42 },
  { key: 'active_rooms', value: 15 },
]);

// Flush remaining events and stop timers
await collector.flush();
collector.destroy();

Configuration

Pass a CollectorConfig object to the constructor:

| Option | Type | Default | Description | |--------|------|---------|-------------| | baseUrl | string | required | Base URL of the Collector backend (e.g. https://analytics.cherry.fun). | | apiKey | string | required | Project API key from the admin panel. | | platform | string | undefined | Client platform (e.g. ios, android, web, server). Attached to all events. | | version | string | undefined | Application version string (e.g. 2.1.0). Attached to all events. | | batchSize | number | 10 | Number of events per batch before auto-flush. | | flushIntervalMs | number | 5000 | Milliseconds between automatic batch flushes. | | configRefreshSec | number | 300 | Seconds between config refreshes from server. | | debug | boolean | false | Enable debug logging to console.debug. | | onError | function | undefined | Called when network or API errors occur. Receives (error, context). | | onSuppressed | function | undefined | Called when an event is dropped by a filter rule (client-side or server-side). Receives (event, pattern, origin) where origin is 'client' or 'server'. Applies to all event types. |

API Reference

Lifecycle

init(): Promise<void>

Initialize the client. Fetches filter configuration from the server and starts the periodic config refresh timer. Must be called before sending events.

await collector.init();

flush(): Promise<void>

Flush all queued events to the server immediately. Returns when the send completes (or fails silently).

await collector.flush();

destroy(): void

Stop all timers and flush remaining events. Call before application exit or when the client is no longer needed.

collector.destroy();

Context

These setters attach context to all subsequent events:

setUser(userId: string): void

Set the user ID.

collector.setUser('wallet_address_or_user_id');

setSession(sessionId: string): void

Set the session ID.

collector.setSession('session_uuid');

setModule(module: string): void

Set the default module name.

collector.setModule('messaging');

setDevice(deviceId: string): void

Set the device ID.

collector.setDevice('device_uuid_or_id');

Errors

error(message: string, params?: Record<string, unknown> & { stackTrace?: string }): void

Send an error event. The special stackTrace key is extracted and sent as the structured stackTrace.raw field.

try {
  await fetchData();
} catch (e) {
  collector.error('Failed to fetch data', {
    url: '/api/data',
    stackTrace: e instanceof Error ? e.stack : undefined,
  });
}

Logging

log(level: LogLevel, message: string, params?: Record<string, unknown>): void

Send a log event with an explicit level (fatal, error, warn, info, or debug).

collector.log('warn', 'Slow query detected', { duration: 2500 });

fatal(message: string, params?: Record<string, unknown>): void

Shorthand for log('fatal', message, params).

collector.fatal('Critical system failure');

error(message: string, params?: Record<string, unknown>): void

Shorthand for log('error', message, params). Note: this is different from the error() method above, which has special stackTrace handling.

warn(message: string, params?: Record<string, unknown>): void

Shorthand for log('warn', message, params).

collector.warn('High memory usage', { mb: 512 });

info(message: string, params?: Record<string, unknown>): void

Shorthand for log('info', message, params).

collector.info('Server started', { port: 3000 });

debug(message: string, params?: Record<string, unknown>): void

Shorthand for log('debug', message, params).

collector.debug('Cache miss', { key: 'user_settings_123' });

Analytics

analytics(event: string, message: string, params?: Record<string, unknown>): void

Send an analytics event. The event parameter is an event name (e.g. screen_view, button_click), and message is a human-readable description.

collector.analytics('screen_view', 'ChatScreen', {
  fromScreen: 'RoomList',
  roomType: 'dm',
});

collector.analytics('button_click', 'Send message', {
  hasAttachment: true,
  messageLength: 42,
});

Revenue

Revenue is the SDK's single money primitive. Provide a human-readable amount (signed number) — positive = inflow, negative = cost. Negative amounts require a reason qualifier so every cost is categorized. Revenue calls are sent immediately (not queued through flush()) — await them and avoid calling from hot loops without throttling.

For BigInt-precision use cases (whale amounts > 9007 SOL, on-chain reconciliation, future 18-decimal tokens), use rawAmount: string (smallest unit) instead of amount. Exactly one of the two is required.

See docs/decisions/002-revenue-data-model.md, docs/decisions/003-sdk-revenue-api.md, and docs/decisions/004-backend-decimal-registry.md for the full design.

revenue(input: RevenueInput): Promise<void>

Record a single revenue or cost event. Identity (userId, sessionId, deviceId, platform, version) is inherited from the client's context — no need to re-pass.

The canonical positive case — a SOL fee skim from a swap:

import { SolanaTokens } from '@cherrydotfun/collector-sdk';

await collector.revenue({
  source: 'swap',
  amount: 0.005,                      // 0.005 SOL — human-readable
  token: SolanaTokens.WSOL,
  usdValue: 1.25,
  recipientWallet: 'Fee9oABC...DEF',
  txHash: '5xY7zZ...QQ',
  params: { pair: 'SOL/USDC' },
});

Recording a cost

A cost is a revenue event with a negative amount and a required reason. Example: pre-funding a daily prize pool that bet revenue is expected to cover.

await collector.revenue({
  source: 'bet',
  amount: -1000,                      // -1000 USDC (signed number)
  reason: 'jackpot_seeding',          // REQUIRED when amount < 0
  token: SolanaTokens.USDC,
  usdValue: -1000.00,
  recipientWallet: 'PrizePool111...PublicKey',
  txHash: 'abc...',
  params: { campaign: 'daily-bets', expectedCoveragePeriod: '24h' },
});

Precision path — rawAmount for whales / on-chain reconciliation

Use rawAmount instead of amount when you need exact smallest-unit precision. The string carries an integer count of lamports / base units, BigInt-safe (no float rounding). Mutually exclusive with amount.

await collector.revenue({
  source: 'whale_swap',
  rawAmount: '1500000000000',         // exactly 1500 SOL in lamports
  token: SolanaTokens.WSOL,
  usdValue: 375_000,
  txHash: 'whaleTx...QQ',
});

Suggested reason vocabulary: jackpot_seeding, marketing, integration_partner, infra, correction, refund. Free-form — the project owns the list.

For costs, recipientWallet flips semantics: it's the external wallet you paid, not your own.

Fiat / partnership revenue

Off-chain money flows use network: 'fiat', address: null, and an ISO-4217 symbol. Example: a partnership deal paid via wire transfer.

await collector.revenue({
  source: 'partnership',
  amount: 25000,                      // $25,000.00 (human-readable)
  token: {
    symbol: 'USD',
    address: null,
    network: 'fiat',
    decimals: 2,                      // fiat tokens aren't in the registry — caller supplies
  },
  usdValue: 25000.00,
  message: 'Q2 integration partnership: AcmeCorp',
  params: {
    partner: 'AcmeCorp',
    deal: 'Q2-2026-integration',
    invoiceId: 'INV-2026-0042',
    paymentMethod: 'wire',
  },
});

Multi-token revenue from one transaction

When a single on-chain action produces revenue in multiple tokens (e.g. a swap collecting a SOL base fee AND a USDC referral skim), emit one event per token and share the same txHash. This keeps every dashboard GROUP BY query a one-liner — no $unwind step needed to aggregate by token or by source.

const txHash = '5xY7zZ...QQ';

await collector.revenueBatch([
  {
    source: 'swap',
    amount: 0.005,                    // 0.005 SOL
    token: SolanaTokens.WSOL,
    usdValue: 1.25,
    txHash,
  },
  {
    source: 'swap',
    amount: 0.001,                    // 0.001 USDC
    token: SolanaTokens.USDC,
    usdValue: 0.25,
    txHash,
  },
]);

revenueBatch(inputs: RevenueInput[]): Promise<void>

Record multiple revenue/cost events in one request via /api/collect/batch. Sent immediately. Use for high-throughput callers (e.g. swap aggregators batching per-block). Backend accepts up to 100 events per call.

await collector.revenueBatch([
  { source: 'swap', amount: 0.005, token: SolanaTokens.WSOL, usdValue: 1.25 },
  { source: 'swap', amount: 0.0075, token: SolanaTokens.WSOL, usdValue: 1.88 },
  { source: 'vault', amount: 2.5, token: SolanaTokens.USDC, usdValue: 2.50 },
]);

Token helpers

The SolanaTokens constant exposes pre-built TokenRef entries for common Solana tokens. Initial set: WSOL, USDC, USDT, USDG.

import { SolanaTokens } from '@cherrydotfun/collector-sdk';

SolanaTokens.WSOL;  // { symbol: 'SOL',  address: 'So11...112', network: 'solana', decimals: 9 }
SolanaTokens.USDC;  // { symbol: 'USDC', address: 'EPjF...Dt1v', network: 'solana', decimals: 6 }
SolanaTokens.USDT;  // { symbol: 'USDT', address: 'Es9v...wNYB', network: 'solana', decimals: 6 }
SolanaTokens.USDG;  // { symbol: 'USDG', address: '2u1t...jGWH', network: 'solana', decimals: 6 }

For tokens not in the helper, build a TokenRef literal. decimals is now optional — backend fills from its registry when the mint is known:

await collector.revenue({
  source: 'swap',
  amount: 1.5,
  token: {
    symbol: 'JUP',
    address: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN',
    network: 'solana',
    // decimals omitted — backend fills if registry knows this mint;
    // otherwise rejects with a 400 (you'd then either supply decimals
    // or ask an admin to add the mint to the registry).
  },
});

For tokens not in the helper, construct a TokenRef literal:

import type { TokenRef } from '@cherrydotfun/collector-sdk';

const BONK: TokenRef = {
  symbol: 'BONK',
  address: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
  network: 'solana',
  decimals: 5,
};

await collector.revenue({ source: 'swap', amount: '100000000', token: BONK });

For native assets, use the wrapped-native mint as the canonical address (e.g. So11...112 for SOL). Cherry treats native and wrapped as one token family at the analytics layer.

Things to know

  • Immediate, not batched. revenue() and revenueBatch() hit the network on each call and bypass the flush() queue. Always await them, and don't fire them inside hot loops without throttling.
  • reason is required when amount is negative. The server enforces this; missing-reason cost events will be rejected.
  • message is auto-synthesized when omitted. Examples: "Revenue: 0.005 SOL", "Cost (jackpot_seeding): 1000 USDC". Pass message explicitly when you want a custom summary.
  • Wire-protocol naming for raw-HTTP integrators. On the wire, the source field is sent as event (the SDK maps source → event so the storage layer can reuse the existing column). SDK consumers get the source alias for free; only raw-HTTP integrators bypassing the SDK need to know this.
  • Silent suppression by filter rules. A filter rule like revenue.swap or revenue.* will drop matching events without raising an error. Pass an onSuppressed callback (see the Configuration table) to detect these — client-side and server-side suppressions both fire it.

Metrics

Metrics are sent immediately (not batched).

metric(key: string, value: number): Promise<void>

Set a single metric value.

await collector.metric('ws_connections', 42);
await collector.metric('active_rooms', 15);

metricBatch(ops: MetricOp[]): Promise<void>

Set multiple metric values in one request. The backend accepts up to 100 operations per call.

await collector.metricBatch([
  { key: 'ws_connections', value: 42 },
  { key: 'active_rooms', value: 15 },
  { key: 'cpu_usage', value: 34.5 },
]);

Direct Send

send(event: Partial<CollectorEvent> & { type: CollectorEvent['type']; message: string }): Promise<void>

Send a single event immediately via POST /api/collect, bypassing the batch queue. Useful for critical events that must not be delayed.

The event is enriched with context (userId, sessionId, platform, etc.) and filtered against disabled patterns, same as queued events.

await collector.send({
  type: 'error',
  level: 'error',
  message: 'Critical error',
  params: { severity: 'high' },
});

Configuration

getConfig(): ConfigResponse | null

Returns the last successfully fetched server configuration, or null if no config has been loaded yet.

const config = collector.getConfig();
if (config) {
  console.log('Disabled patterns:', config.disabled);
}

refreshConfig(): Promise<void>

Force-refresh the filter configuration from the server. Normally called automatically on the configured interval.

await collector.refreshConfig();

Observability

getStats(): CollectorStats

Returns SDK telemetry counters: events sent, dropped (filtered), failed, and current queue size.

const stats = collector.getStats();
console.log(`Sent: ${stats.sent}, Dropped: ${stats.dropped}, Failed: ${stats.failed}`);

isInitialized: boolean

Read-only getter. Whether init() has been called and completed successfully.

if (collector.isInitialized) {
  console.log('Ready to send events');
}

Error Handling

Pass an onError callback to handle network and API errors:

const collector = new CollectorClient({
  baseUrl: 'https://analytics.cherry.fun',
  apiKey: 'xxxxx',
  onError: (error, context) => {
    console.error(`Collector error in ${context}:`, error.message);
    // Log to your error tracking service, retry, etc.
  },
});

await collector.init();

The context parameter indicates where the error occurred:

  • sendBatch - batch event send
  • send - direct event send
  • metric - metric set
  • metricBatch - batch metric set
  • refreshConfig - config refresh from server

Event Filtering

The SDK fetches a filter configuration from the server on init and periodically refreshes it. Events matching disabled patterns are dropped client-side and not sent.

Patterns are hierarchical:

  • log.debug — disable debug-level log events
  • log.* — disable all log events
  • log — disable all log events (alternate syntax)
  • * — disable all events

When you call collector.debug(...), the SDK checks if log.debug is in the disabled set. If so, the event is dropped locally (not sent), and the dropped counter is incremented.

const stats = collector.getStats();
console.log(`Events dropped: ${stats.dropped}`);

Platform Support

The SDK requires globalThis.fetch to be available:

  • Browser: Native fetch, or polyfill
  • React Native: Built-in fetch (works with http:// and https://)
  • Node.js 18+: Native fetch

For Node.js < 18, provide a fetch polyfill (e.g. node-fetch):

import fetch from 'node-fetch';
globalThis.fetch = fetch;

const collector = new CollectorClient({ /* ... */ });

Types

All types are exported from @cherrydotfun/collector-sdk:

import {
  CollectorClient,
  CollectorConfig,
  CollectorEvent,
  CollectorStats,
  ConfigResponse,
  EventType,
  LogLevel,
  AnalyticsLevel,
  MetricOp,
  IngestResponse,
  BatchIngestResponse,
} from '@cherrydotfun/collector-sdk';

Type Reference

| Type | Description | |------|-------------| | CollectorClient | Main client class for sending events and metrics. | | CollectorConfig | Configuration options for the client. | | CollectorEvent | Event payload sent to the backend. | | CollectorStats | SDK telemetry counters (sent, dropped, failed, queueSize). | | ConfigResponse | Server response from GET /api/config with disabled patterns. | | EventType | Union of 'error' \| 'log' \| 'analytics'. | | LogLevel | Union of 'fatal' \| 'error' \| 'warn' \| 'info' \| 'debug'. | | AnalyticsLevel | Union of well-known analytics events or any custom string. | | MetricOp | Single metric key-value pair for batch operations. | | IngestResponse | Response from single event send. | | BatchIngestResponse | Response from batch event send. |

React Native Example

import { useEffect } from 'react';
import { AppState } from 'react-native';
import { CollectorClient } from '@cherrydotfun/collector-sdk';

const collector = new CollectorClient({
  baseUrl: 'https://analytics.cherry.fun',
  apiKey: 'xxxxx',
  platform: 'android', // or 'ios'
  version: '2.1.0',
  debug: false,
  onError: (error, context) => {
    console.error(`Collector error [${context}]:`, error);
  },
});

export function App() {
  useEffect(() => {
    const init = async () => {
      await collector.init();
      collector.setUser(userWallet);
      collector.setSession(sessionId);
      collector.setModule('chat');
    };

    init();

    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'background') {
        collector.flush();
      }
    });

    return () => {
      subscription.remove();
      collector.flush();
      collector.destroy();
    };
  }, []);

  return (
    // Your app components
  );
}

Node.js Server Example

import { CollectorClient } from '@cherrydotfun/collector-sdk';

const collector = new CollectorClient({
  baseUrl: 'https://analytics.cherry.fun',
  apiKey: 'xxxxx',
  platform: 'server',
  version: '1.0.0',
  debug: process.env.DEBUG === 'true',
});

await collector.init();

// Log application startup
collector.info('Server started', { port: 3000, env: process.env.NODE_ENV });

// Track unhandled errors
process.on('uncaughtException', (error) => {
  collector.error('Uncaught exception', {
    name: error.name,
    message: error.message,
    stackTrace: error.stack,
  });
  collector.flush().finally(() => process.exit(1));
});

// On graceful shutdown
process.on('SIGTERM', async () => {
  console.log('Shutting down...');
  await collector.flush();
  collector.destroy();
  process.exit(0);
});

Build Distribution

The SDK is published in multiple formats:

  • CommonJSdist/index.js for Node.js
  • ES Modulesdist/index.mjs for bundlers
  • TypeScript Declarationsdist/index.d.ts

License

MIT