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

@analytiics/sdk-node

v0.2.0

Published

Analytiics server SDK: revenue attribution and identity from your backend.

Readme

@analytiics/sdk-node

Authenticated server events: revenue, identity links, and backend activity.

The browser tracker posts to a public, unauthenticated endpoint — it has to, since the snippet ships to every visitor. That endpoint therefore ignores revenue: if it didn't, anyone could fill a customer's dashboard with fake sales. Revenue arrives here instead, over an authenticated path.

Setup

Each project gets a server write key. It is shown once, when it is issued, and cannot be recovered afterwards — only its hash is stored.

# on the app that sends server events
ANALYTIICS_SITE=liinks.co
ANALYTIICS_WRITE_KEY=sk_live_...

Keys are bound to a project, so a leaked key can only write to the project it was issued for, and a key can be revoked without redeploying anything. Keep it server-side — it is not a publishable key.

import { createClient } from "@analytiics/sdk-node";

export const analytiics = createClient({
  site: "liinks",
  writeKey: process.env.ANALYTIICS_WRITE_KEY!,
  api: "https://in.analytiics.co",
});

Attributing revenue to a channel

A Stripe webhook knows a customer id and an amount. It has no cookie, no referrer, and no session — nothing that says the customer arrived from Google three days ago. The link is the user id, in two steps:

1. In the browser, once the user is known:

window.analytiics.identify(user.id);

That ties the anonymous visitor to a user id, which is what lets a later server-side event find the session — and therefore the source — that earned it. Call it at signup and on login.

2. In the webhook:

export async function POST(req: Request) {
  const event = stripe.webhooks.constructEvent(/* … */);

  const delivery = createClient({
    site: "liinks.co",
    writeKey: process.env.ANALYTIICS_WRITE_KEY!,
  });

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    delivery.revenue({
      name: "subscription_started",
      eventId: event.id, // same provider ID on every webhook replay
      ts: new Date(event.created * 1000).toISOString(),
      userId: session.client_reference_id!, // the same id you passed to identify()
      revenueCents: session.amount_total!,
      currency: session.currency!.toUpperCase(),
      props: { plan: "pro" },
    });
  }

  // Serverless runtimes can freeze the process the moment this resolves.
  try {
    await delivery.flush({ throwOnError: true });
  } catch {
    return new Response("Retry this webhook", { status: 503 });
  }
  return new Response(null, { status: 200 });
}

revenue_by_source then resolves userId → the session that user was first seen in → that session's entry source, and credits the revenue there. Revenue from a user who was never seen in a browser session is reported as Unattributed rather than dropped. Revenue with no user ID is also included there, without inventing a customer identity.

Credit goes to the first session, not the last: the channel that found the customer is the one worth spending on.

What this can't do

Visitor ids rotate daily (they're a hash of IP + user agent, not a cookie), so a user who first visits on Monday and signs up on Wednesday can only be linked back to Wednesday's session. Attribution uses the first observed identified session. A visit on a previous day that never carried an identity cannot reliably be linked to that user. Known-user funnel steps use the user ID across days and devices.

With the generated helpers

If the project has an analytics.yaml, prefer the typed helpers over raw event names — that's the point of the manifest:

import { createClient, toAnalytiicsClient } from "@analytiics/sdk-node";
import { createTrack } from "./analytics.gen.js";

const analytiics = createClient({ site: "thiings", writeKey: process.env.ANALYTIICS_WRITE_KEY! });
// Inside the request handler after resolving its user:
const track = createTrack(toAnalytiicsClient(analytiics, { userId: session.userId }));

// `revenue: true` in the manifest makes the amount part of the signature —
// recording the conversion without the money won't compile.
track.purchase(
  { type: "personal_indie" },
  { userId, revenueCents: 4900, currency: "USD" },
);
await analytiics.flush();

The generated track() signature has nowhere to put a user id; it's shaped for the browser, where the tracker already knows who it is. On a server there's no ambient user, so toAnalytiicsClient takes one and attaches it to every non-revenue event. Regenerate with analytiics codegen to get createTrack. Never call the shared browser configure() with request-specific identity.

API

client.track({ name, eventId?, userId?, props?, url?, ts? });
client.identify(userId);
client.revenue({ name, eventId?, userId?, revenueCents, currency, props?, ts? });
await client.flush();   // send now — required before a serverless handler returns
await client.close();   // flush and stop the timer
await client.flush({ throwOnError: true }); // require confirmed delivery

The default flush reports failures through onError and resolves, preserving best-effort analytics for application traffic. Strict flush rejects if any event on this client failed delivery, including an earlier automatic flush. That failure stays on the client: a later successful batch cannot repair a lost event. Use a separate client per webhook handler when requiring strict delivery.

Batches retry twice on network errors, HTTP 429 and 5xx. Each attempt has a 25-second timeout (requestTimeoutMs configures it). Other 4xx errors are reported immediately. Each queued event gets a UUID and timestamp once, before any retry; properties are copied at enqueue time. Batches never exceed 200 events.

For provider webhooks, supply eventId: event.id and the original ts so a new handler invocation reuses the logical event's identity. If one provider event produces multiple analytics events, suffix its ID with each event's purpose. The collector returns 409 for conflicting reuse. Server event IDs are required on the wire; this SDK supplies them automatically. Older SDKs must be upgraded before switching to the new collector contract. $ event names are reserved except $identify.

The production collector persists a receipt and waits for Tinybird's database commit before reporting acceptance. Retries reuse the first payload/timestamp; queries and aggregate states deduplicate IDs after uncertain writes. Without an acknowledged response, retry using the same IDs. This is not an autonomous outbox: the provider or caller must retry a failed delivery.

revenueCents may be negative — refunds and chargebacks are real revenue events. A currency is required whenever an amount is set: an amount with no unit can't be summed, and defaulting to USD would quietly misreport every non-USD customer.

Serverless

For best-effort application events a module-scoped client is fine. For payment webhooks use the request-local client and strict flush shown above. Always await the flush before returning. The default batching (20 events or 5 seconds) exists for long-running servers; in a function that freezes between invocations, an unflushed batch is a lost sale.