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

@happyvertical/smrt-marketing

v0.51.10

Published

Cross-channel campaign coordination, immutable performance evidence, budget pacing, and reusable Svelte marketing surfaces for SMRT

Readme

@happyvertical/smrt-marketing

Cross-channel Campaign models, immutable performance snapshots, computed budget pacing, and reusable Svelte marketing surfaces for s-m-r-t.

pnpm add @happyvertical/smrt-marketing
import {
  BudgetPacingService,
  CampaignChannelCollection,
  CampaignCollection,
  MetricIngestionService,
} from '@happyvertical/smrt-marketing';

const campaigns = await CampaignCollection.create({ db });
const channels = await CampaignChannelCollection.create({ db });
const campaign = await campaigns.create({
  tenantId,
  customerId,
  campaignKey: 'summer-demand-2026',
  name: 'Summer demand 2026',
  objective: 'demand_generation',
  budgetCents: 200_000,
  currency: 'CAD',
});
if (!campaign.id) throw new Error('Campaign did not persist');

const adGroup = await channels.create({
  tenantId,
  campaignId: campaign.id,
  channelKind: 'ad_group',
  channelRef: 'ad-group-42',
  allocatedBudgetCents: 150_000,
});
if (!adGroup.id) throw new Error('Campaign channel did not persist');

const ingestion = await MetricIngestionService.create({ db });
await ingestion.ingest({
  tenantId,
  campaignId: campaign.id,
  campaignChannelId: adGroup.id,
  periodStart: new Date('2026-07-01T00:00:00Z'),
  periodEnd: new Date('2026-07-01T23:59:59Z'),
  spendCents: 12_500,
  impressions: 25_000,
  clicks: 800,
  conversions: 35,
  leads: 20,
  source: 'ad-platform',
  dedupeKey: `${tenantId}:summer-demand-2026:ad-group-42:2026-07-01`,
});

// Channel-scoped evidence is accepted only when the channel belongs to the
// supplied campaign. Reporting periods are required valid date-like values.

const pacing = await BudgetPacingService.create({ db });
console.log(await pacing.getCampaignPacing(campaign.id));

Customer-scoped campaign reads

Campaign.customerId is the native UUID relationship to the canonical @happyvertical/smrt-commerce:Customer. A campaign and its Customer must have exactly the same tenant, and customer-scoped reads require that tenant explicitly (null selects the global/global scope). Associated Campaign saves validate and persist in one transaction; customer-scoped reads validate and query in one transaction. Missing and cross-tenant Customers fail with CampaignCustomerScopeError without disclosing which condition occurred.

const firstPage = await campaigns.listByCustomer(tenantId, customerId, {
  limit: 50,
});
const secondPage = firstPage.nextCursor
  ? await campaigns.listByCustomer(tenantId, customerId, {
      limit: 50,
      after: firstPage.nextCursor,
    })
  : null;

const summaries = await campaigns.summarizeByCustomers(tenantId, customerIds);
// [{ customerId, totalCount, activeCount, latestStartAt }]

const reporting = await campaigns.listReportingByCustomer(
  tenantId,
  customerId,
  {
    limit: 50,
    after: firstPage.nextCursor ?? undefined,
    at: new Date('2026-08-15T00:00:00Z'),
  },
);
// reporting.items keeps the same newest-first page order. Every item contains:
// { campaign, channelCount, channelMix, metricTotals, pacing }

Pages, reporting pages, and summary batches are capped at 100 items and reject larger inputs. Pagination is newest-first by startAt, then UUID; campaigns without a start time follow scheduled campaigns. listReportingByCustomer() validates Customer scope and reads the page in one transaction, then performs one grouped channel read and one grouped immutable-evidence read regardless of page size. metricTotals use the same evidence selection as BudgetPacingService: for each exact period, a campaign rollup replaces its channel snapshots while channel-only periods remain. pacing is therefore equivalent to getCampaignPacing() without per-campaign callbacks or lazy loads. Summary resolution likewise uses a bounded grouped query rather than loading tenant campaigns or issuing one query per Customer.

Migrating metadata-backed associations

  1. Apply the generated schema migration that adds nullable native-UUID campaigns.customer_id and the (tenant_id, customer_id, start_at, id) index.
  2. In an operator-owned data migration, extract the old metadata Customer id, validate that it exists in commerce and has the exact same tenant_id, then write customer_id. Stop on missing, malformed, or mismatched values.
  3. Verify every expected association through listByCustomer() or summarizeByCustomers(), then update consumers to use these APIs.
  4. Remove the old metadata key after verification. Marketing never reads it as a compatibility fallback, so there is no tenant-wide JSON or raw-SQL path to keep in sync.

Svelte components are exported from @happyvertical/smrt-marketing/svelte. They are presentational and accept plain view models; consumers remain in control of fetching and mutations.

See AGENTS.md for lifecycle, evidence, and package-boundary invariants.