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

@capitalthought/observability

v0.3.1

Published

Shared observability for Capital Thought projects — Sentry shim + structured logger + Axiom Logpush config builder.

Downloads

582

Readme

@capitalthought/observability

Shared observability for Capital Thought projects — Sentry shim + structured logger + Axiom Logpush config builder.

Why

The same three pieces of observability glue are currently duplicated across the portfolio:

| Repo | Sentry init | Structured logger | Axiom Logpush dest builder | |---|---|---|---| | mikey | ✅ src/shared/sentry.ts | ✅ src/shared/logger.ts | inline in CLAUDE.md | | multipov | ✅ inline | ✅ inline | inline | | pitch2 | ✅ Next.js variant | ✅ inline | inline | | bpm, actualjob, doorman, personalize, highfives | partial / mixed | mixed | inline / missing |

Each copy is slightly different. Each one is missing something different. None share fixes to the no-op-when-DSN-unset behavior, the breadcrumb integration, or the Logpush destination URL builder.

This package consolidates the three pieces and exposes them through a runtime-adapter pattern. Cloudflare Workers, Next.js (Vercel), and plain Node consumers all use the same createSentry() and createLogger() API; the adapter at the seam is the only thing that differs per runtime.

Phase 1 — what's in this commit

| Module | Status | |---|---| | logger.tscreateLogger({ name?, level? }) JSON-line logger | ✅ shipped | | axiom.tsaxiomLogpushDestination() URL builder | ✅ shipped | | sentry.ts — common SentryShim interface | ✅ shipped | | adapters/cloudflare.ts — Workers adapter using @sentry/cloudflare | ✅ shipped (lifted from mikey) | | adapters/nextjs.ts — Next.js adapter using @sentry/nextjs | 🟡 stub — Phase 2 lift from pitch2 | | adapters/node.ts — plain-Node adapter using @sentry/node | 🟡 stub — Phase 2 |

Phase 1 commits zero runtime risk. No consumer changes. Only mikey is in the lift-from set today; the package is the canonical home for the next round.

Migration phases

  1. Phase 1 (this scaffold). Create the package + interface + Cloudflare adapter (lifted from mikey) + tests. Stub nextjs and node adapters. No consumer changes.
  2. Phase 2. Migrate mikey to consume the package (drops mikey/src/shared/sentry.ts + mikey/src/shared/logger.ts). Lift pitch2's Next.js Sentry init into adapters/nextjs.ts. Migrate pitch2.
  3. Phase 3. Lift the multipov / bpm / actualjob / doorman / personalize / highfives copies. By end of phase, every repo with observability uses this package and only this package.
  4. Phase 4. Add Logpush job creation helpers (CF API), so a consumer can run await createLogpushJob({ ... }) instead of clicking through the Cloudflare dashboard.

Each consumer migrates on its own PR, on its own timeline.

Design decisions (2026-05-02)

Adapter pattern, not feature flags

Three Sentry SDKs ship as different npm packages: @sentry/cloudflare, @sentry/nextjs, @sentry/node. The same wrapper API (createSentry(...)) routes to whichever adapter the consumer imports. This keeps the package's dependencies empty (or peerDeps-only) so a Workers consumer doesn't drag in @sentry/node, and a Next.js consumer doesn't drag in @sentry/cloudflare.

The adapter contract lives in src/adapters/types.ts. Each adapter exports a createSentry(opts) factory matching that contract. Phase 1 implements only cloudflare; the other two throw a clear "not yet implemented" error at construction time so a misconfigured consumer fails loud at startup, not silent at runtime.

No-op when DSN unset

Every adapter must return a working shim even when dsn is undefined. This is non-negotiable — local dev, CI, and preview deploys often run without Sentry wired. The shim's wrapHandler becomes a passthrough; captureException and captureMessage no-op; addBreadcrumb no-ops. The Sentry SDK's own captureException is already a no-op when no client is initialized, so accidental calls before init are safe.

This is the canonical behavior lifted from mikey/src/shared/sentry.ts (commit ac76a2d, shipped 2026-05-02).

Logger is runtime-agnostic

The logger writes JSON lines to console.log / console.warn / console.error. That works in Workers, Node, and the browser equally well. Cloudflare Workers Observability scrapes stdout into Logpush, Vercel scrapes stdout into Vercel Logs, plain Node scrapes stdout into whatever process supervisor is running it. No runtime-specific transports — just stdout.

When a Sentry shim is also configured, the logger optionally adds breadcrumbs on warn and error calls. This is opt-in via createLogger({ sentry }) — the logger doesn't grow a hard dependency on Sentry.

Axiom destination builder, not runtime ingest

The package builds the destination_conf URL string for a Cloudflare Logpush job pointing at Axiom. It does NOT ingest logs at runtime — that's Cloudflare's job, downstream of console.log. The builder exists because the URL format (https://api.axiom.co/v1/datasets/<dataset>/ingest?timestamp-field=<field> with the bearer token folded into the destination_conf header) has bitten every consumer at least once.

Future Phase 4 may add a createLogpushJob(api, opts) that uses this builder to PUT a Logpush job via the Cloudflare API. Phase 1 just gives you the string.

Usage (once Phase 2 lands)

// Cloudflare Worker
import { createSentry, createLogger, axiomLogpushDestination } from "@capitalthought/observability";
import { createCloudflareAdapter } from "@capitalthought/observability/adapters/cloudflare";

const sentry = createSentry({
  adapter: createCloudflareAdapter(),
  dsn: env.SENTRY_DSN,    // optional — no-op when undefined
  workerName: "mikey",
});

const log = createLogger({ name: "shiny-cron", sentry });

export default sentry.wrapHandler({
  async scheduled(event, env, ctx) {
    log.info("cron tick", { cron: event.cron });
    try {
      // ... do work ...
    } catch (err) {
      log.error("cron failed", { err: String(err) });   // adds Sentry breadcrumb
      sentry.captureException(err, { cron: event.cron });
      throw err;
    }
  },
});

// And the Axiom Logpush destination_conf string for `wrangler` or the CF API:
const dest = axiomLogpushDestination({
  dataset: "mikey-prod",
  ingestToken: env.AXIOM_INGEST_TOKEN,
  timestampField: "EventTimestamp",
});
// → "https://api.axiom.co/v1/datasets/mikey-prod/ingest?timestamp-field=EventTimestamp&header_Authorization=Bearer%20<token>"

v0.2.0 — three new APIs (2026-05-04)

Cron-monitor heartbeats — sentry.captureCheckIn(...)

Cloudflare Workers with cron triggers report cron-monitor heartbeats via Sentry. The shim now exposes captureCheckIn so consumers don't need @sentry/cloudflare as a direct dep.

const checkInId = sentry.captureCheckIn(
  { monitorSlug: "radar-meeting-prep-daily", status: "in_progress" },
  {
    // schedule on the FIRST check-in auto-upserts the monitor in Sentry's UI.
    schedule: { type: "crontab", value: "0 11 * * *" },
    checkinMargin: 5,
    maxRuntime: 30,
    timezone: "UTC",
  },
);

ctx.waitUntil(
  runJob().then(
    () => sentry.captureCheckIn({ monitorSlug: "radar-meeting-prep-daily", checkInId, status: "ok" }),
    (err) => {
      sentry.captureCheckIn({ monitorSlug: "radar-meeting-prep-daily", checkInId, status: "error" });
      sentry.captureException(err);
    },
  ),
);

The no-op shim returns "" for checkInId, so two-stage callers can thread the value through unchanged when DSN is unset.

Env-callback Sentry — createCloudflareSentryFromEnv((env) => opts)

Cloudflare Workers can only read secrets from env.X inside a handler, not at module load. Use this factory when DSN lives in an env binding:

import { createCloudflareSentryFromEnv } from "@capitalthought/observability/adapters/cloudflare";

const sentry = createCloudflareSentryFromEnv<Env>((env) => ({
  dsn: env.SENTRY_DSN,           // optional — undefined → SDK no-op
  workerName: env.OBS_WORKER,
  tracesSampleRate: 0.1,
}));

export default sentry.wrapHandler({
  async fetch(req, env, ctx) { ... },
  async scheduled(event, env, ctx) { ... },
});

This mirrors @sentry/cloudflare's native withSentry((env) => opts, handler) lazy-init contract — the SDK runs the callback once per isolate on the first request and initializes there. enabled is always true on this variant (the env callback hasn't run at construction time, so we can't gate). For DSN-known-up-front gating, keep using createCloudflareSentry({ dsn }).

Legacy field-name compat — createLogger({ legacyFieldNames })

Migrating from a hand-rolled logger that emitted different JSON keys (e.g. job instead of name) breaks downstream Axiom dashboards and Sentry filters. The opt-in legacyFieldNames mapping renames keys in the emitted JSON without changing call-site code:

const log = createLogger({
  name: "router",
  legacyFieldNames: { name: "job" }, // RR's pre-package convention
});
log.info("hi");
// → {"ts":"...","level":"info","message":"hi","job":"router"}

You can remap any of name, correlationId, level, message, ts. Default behavior is unchanged from v0.1.x — purely additive.

Scripts

npm run check    # tsc --noEmit
npm run build    # tsc → dist/
npm run test     # vitest run

Related

  • mikey/src/shared/sentry.ts — canonical Workers Sentry init (lifted into adapters/cloudflare.ts)
  • mikey/src/shared/logger.ts — canonical structured logger (lifted into logger.ts)
  • mikey/CLAUDE.md § "Axiom (log aggregation via Logpush)" — the Logpush destination URL spec