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

@avada-falcon/aeo-measure-client

v0.13.0

Published

Client for the AEO measurement service: typed calls, three-state rendering, webhook verification.

Readme

@avada-falcon/aeo-measure-client

Published on npmjs.com, public, in the avada-falcon org: npm install @avada-falcon/aeo-measure-client. To release, bump version in this folder's package.json, merge to master and push the tag client-v<version>: the CI job publish_client publishes it (prepack builds lib/ and types/).

CommonJS (lib/) + declarations (types/), the same layout as @avada/utils, because AEO's functions are compiled by Babel to CommonJS. No runtime dependencies (Node ≥ 18.17).

What it is for

Wrapping twelve endpoints is not the reason. fetch does that. It exists for the three things that break at the consuming app, where the service cannot protect them:

| | | |---|---| | Three-state measures | Read a Measure with render() / percent() / isMeasured(), never .rate. m.rate ?? 0 prints 0% for a brand that was never measured. | | Webhook verification | Constant-time, replay window, secret rotation. Getting one of those wrong accepts forged events. | | Tenant boundary | app is fixed when the client is created. Calling code passes only its own shop id and cannot address a shop through another app. |

In AEO

// packages/functions/src/services/aeoMeasure.js
import {createClient} from '@avada-falcon/aeo-measure-client';

export const measure = createClient({
  baseUrl: process.env.AEO_MEASURE_URL,
  apiKey: process.env.AEO_MEASURE_KEY, // `npm run key -- aeo` on the service
  app: 'aeo'
});
// packages/functions/src/controllers/aeoMeasureController.js
import {getCurrentShop} from '@functions/helpers/auth';
import {render, percent, isMeasured, ConflictError} from '@avada-falcon/aeo-measure-client';
import {measure} from '@functions/services/aeoMeasure';

export async function getVisibility(ctx) {
  const shopId = getCurrentShop(ctx); // never from the request
  const m = await measure.metrics(shopId);
  return (ctx.body = {
    label: render(m.visibility), // "78.2% (n=64)" (engines weighted) or "Not measured — 1/3 cycles"
    percent: percent(m.visibility), // number, or null when not measured
    answers: isMeasured(m.visibility) ? m.visibility.count : null // real answers behind it
  });
}

export async function startMeasurement(ctx) {
  const shopId = getCurrentShop(ctx);
  try {
    return (ctx.body = await measure.startCycle(shopId, {newCycle: true}));
  } catch (err) {
    // cycle_running or too_soon. There is no monthly cap to hit (0.7.0: no 402 any more).
    if (err instanceof ConflictError) {
      ctx.status = 409;
      return (ctx.body = {code: err.code, message: err.message});
    }
    throw err;
  }
}

0.11.0

  • trends(ref): the visibility trend of every measured question set (active and retired, newest first), each computed on its own set as metrics(ref, { series }) would. For a chart with one line per set.

0.10.0

  • createDraft(ref, { from }): start the draft from an earlier question set (uuid or version) instead of the active one, to measure with it again. It replaces an open draft; activating it makes a new version. NotFoundError for a set the workspace does not have.

0.9.1

  • TopicTemplate gains matches: string[]: lower-case words that identify the industry in a category text. Match a category to a template on label or any matches entry, at word boundaries (service docs/api.md, GET /v1/topic-templates).

0.9.0

  • An error body that is not JSON (a proxy's 502/504 HTML page) throws ApiError with body: { message: <the text> }, not a SyntaxError.
  • discardDraft() and saveTopics() are no longer retried. A retry after a lost 204 threw NotFoundError for a draft that was already gone, and a retried saveTopics() re-ran the whole re-parse. After a timeout, read topics() or prompts() before sending it again.
  • webhooks.register() no longer declares active, which that response never carried.
  • Service side, no client change: sources.ownSiteCited now counts the store's subdomains too (ir.allbirds.com for allbirds.com), so it can read higher than before.

0.8.0

  • Engines are combined with fixed weights: ChatGPT, asked once a question after its first scan, keeps the weight of a full scan (service docs/api.md, "How engines are combined").
  • Measure (measured) gains count, the real answers behind the rate, and weighted. When weighted is true, numerator/denominator are weighted sums: show the rate with count, not as a fraction. render() prints 78.2% (n=64) for it. byEngine stays unweighted.
  • TrendPoint gains rate, count and weighted: plot rate, don't divide.
  • shareTrend.points[].rows[] gains rate, the share by weight (null when no brand was named).
  • sentiment.byEntity[] gains split: positive/neutral/negative as Measures, by weight.

0.7.0

  • quote(): budget, fitsBudget and shortfall are gone; the service enforces no monthly cap. An engine's first scan of a question set can ask more often than its later scans, so each engine line gains firstScan, samples, later and perMonth, and the quote gains perLaterCycle. answers, usd, liveUsd and perCycle are the NEXT scan.
  • BudgetExceededError is deprecated: the service no longer answers 402.

Receiving webhooks

// packages/functions/src/controllers/aeoMeasureWebhookController.js
import {verifyWebhook, WebhookVerificationError} from '@avada-falcon/aeo-measure-client';
import publishTopic from '@functions/helpers/pubsub/publishTopic';

export async function onMeasureEvent(ctx) {
  let event;
  try {
    event = verifyWebhook({
      // RAW body. On Firebase Functions it lives on the Node request — the same
      // `ctx.req.rawBody` AEO's middleware/validateAction.js already signs against.
      // A re-serialised JSON body will not verify.
      payload: ctx.req.rawBody,
      headers: ctx.headers,
      secret: process.env.AEO_MEASURE_WEBHOOK_SECRET
    });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return (ctx.status = 401);
    throw err;
  }

  // Deliveries retry until acknowledged, so the same event can arrive twice.
  // Deduplicate on event.id, acknowledge fast, and do the work on Pub/Sub.
  // Publish the REFERENCE, not a payload: the subscriber calls measure.metrics().
  await publishTopic('aeoMeasureCycleSettled', {
    eventId: event.id,
    shopId: event.data.workspace.ref,
    cycleId: event.data.cycle.id
  });
  return (ctx.status = 204);
}

For a caller that can itself redeliver — a Pub/Sub subscriber calling startCycle — pass idempotencyKey: message.id, or every redelivery looks like a new request.