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

@partner-api/logger

v3.0.0

Published

Logging module for partner API — runs on Node 20+, Deno, Bun and Workers

Downloads

620

Readme

Partner API Logger SDK (TypeScript)

Send structured logs and metrics to the Partner API ingest service.

Zero dependencies. Runs anywhere there is a global fetch and Web Crypto — Node 20+, Deno, Bun, Cloudflare Workers, Vercel Edge, and Node-based serverless runtimes such as Firebase Cloud Functions.

Installation

pnpm add @partner-api/logger          # Node, Bun, bundlers
deno add npm:@partner-api/logger      # Deno

Both a CommonJS and an ESM build ship in the package; require and import each resolve to the right one.

Usage

import { Logger } from '@partner-api/logger';

const logger = new Logger({
  tenantToken: process.env.PARTNER_API_TENANT_TOKEN!,
  baseUrl: 'https://ingest.partnerapi.com', // optional
  onError: (event) => console.error(event.message), // optional
});

// Buffered. Returns immediately — no network on your request path.
logger.info(apiKey, 'Order created', { orderId: order.id });

Log calls never perform I/O and never throw or reject. They append to an in-process buffer that drains in the background; the only way a delivery failure reaches you is the onError hook. On a runtime that suspends your instance after the response, await logger.flush() before returning — see Delivery.

See packages/logger-spec/spec.md for the full cross-language behaviour contract.

Runtime support

| Runtime | Supported | Notes | | -------------------------------- | --------- | ------------------------------------- | | Node 20+ | yes | Both require and import | | Firebase Cloud Functions (gen 2) | yes | Node 20/22 — flush before returning | | Deno | yes | npm:@partner-api/logger | | Supabase Edge Functions | yes | Deno runtime — see quickstart below | | Bun | yes | | | Cloudflare Workers / Vercel Edge | yes | | | Node 18 | untested | Works, but EOL — see below | | Node 16 and below | no | No global fetch |

engines declares >=20. That is a support policy, not a technical floor: the code does run on Node 18, which has both fetch and crypto.randomUUID — but 18 is end-of-life and is not covered by CI, so nothing keeps it working. Install with --ignore-engines (or engine-strict=false) if you need it anyway.

Egress proxies need a transport

If your logs leave through an HTTP proxy, read this before upgrading. v1's axios read HTTP_PROXY / HTTPS_PROXY / NO_PROXY from the environment automatically. The platform fetch does not, on any runtime, and v2 has no dependency that could. Upgrading behind a proxy makes log delivery fail silently — the env vars keep being set and stop being honoured.

Pass a transport that proxies:

import { fetch as undiciFetch, ProxyAgent } from 'undici';

const dispatcher = new ProxyAgent(process.env.HTTPS_PROXY!);

const logger = new Logger({
  tenantToken,
  fetch: (url, init) => undiciFetch(url, { ...init, dispatcher }),
});

On Deno, DENO_CERT / --proxy handle this at the runtime level and no override is needed.

Delivery: buffered and non-throwing

Telemetry must not be able to break, slow, or fail the request it describes. So (since v3) every log method buffers:

  • info / warn / error / debug / logRequest / logResponse append to an in-memory queue and return an already-resolved promise. Awaiting them is harmless and instant; not awaiting them is also safe — there is no rejection to go unhandled.
  • The queue drains when it reaches batchSize entries, every flushIntervalMs, and whenever you call flush(). Each drain sends one POST per batch instead of one POST per line, split so no request exceeds ingest's limit of 1000 entries per request. (Ingest also accepts a 5 MB JSON body and truncates any single entry over 250 KB; on top of that the SDK caps each request at ~1 MB of log lines, which keeps it well clear of the body limit and keeps one failed batch cheap to retry.)
  • Failed batches are retried with jittered exponential backoff. A batch that still cannot be delivered is dropped and reported to onError — never raised at whatever code happened to log at the time.
  • The buffer is fixed-size. When it is full the oldest entries are dropped first, counted, and reported.

metric() / metrics() are not buffered: a metric submission is an explicit write you are entitled to a receipt for, so it awaits the POST and throws on failure exactly as before.

Flush before the instance goes away

On any runtime that suspends or destroys the instance once the response is returned — Firebase Cloud Functions, Lambda, Cloud Run with CPU throttling, Vercel/Supabase Edge — drain the buffer before returning:

export const handler = onRequest(async (req, res) => {
  const correlationId = await logger.logRequest(apiKey, {
    method: req.method,
    path: req.path,
  });

  const body = await doWork();

  logger.logResponse(apiKey, {
    statusCode: 200,
    duration: Date.now() - start,
    correlationId,
  });

  await logger.flush(); // one batched POST for the whole request
  res.json(body);
});

flush() resolves once everything buffered at call time has been delivered or given up on. It never rejects. Where the platform offers a waitUntil, hand it logger.flush() instead and keep even that off the response path.

Use shutdown() (alias close()) on a long-lived process's exit path: it stops the periodic timer and flushes. The logger stays usable afterwards — a later log call re-arms the timer.

Options

| Option | Default | What it does | | ----------------- | ------- | --------------------------------------------------------------------- | | onError | console.error of the message | Receives every drop: { reason, message, entryCount, status?, attempts?, retryable?, droppedTotal, cause? }. reason is flush-failed, buffer-overflow or invalid-entry. | | batchSize | 100 | Buffered entries that trigger an automatic flush. Capped at 1000 (the ingest per-request maximum) and at maxBufferSize. | | flushIntervalMs | 2000 | Milliseconds between automatic flushes. 0 disables the timer, leaving batchSize and flush() as the triggers. | | maxBufferSize | 1000 | Entries held before the oldest are dropped. Bounds memory while ingest is unreachable. | | maxRetries | 3 | Retries per batch after the first attempt. Network faults, 408, 429 and 5xx are retried; any other 4xx is a bad request and is dropped immediately. | | retryBaseDelayMs| 200 | First backoff step. Equal jitter: half the window is the exponential step, half is random. | | retryMaxDelayMs | 5000 | Backoff ceiling. | | requestTimeoutMs| 5000 | Per-request deadline. Bounds how long an awaited flush() can hold a response — without it a blackholed ingest leaves the request pending forever. 0 disables it. Needs AbortSignal.timeout; where the runtime lacks it the SDK runs without a deadline. |

logger.stats() returns { buffered, delivered, dropped } if you want to export the SDK's own health as a metric.

const logger = new Logger({
  tenantToken,
  batchSize: 50,
  flushIntervalMs: 5000,
  maxBufferSize: 5000,
  onError: (event) => {
    metrics.increment('partner_api.logs.dropped', event.entryCount);
    console.error(event.message);
  },
});

Supabase Edge Functions quickstart

Supabase Edge Functions run on Deno, so there is no install step and no bundler — import through the npm: specifier and the runtime fetches the package, reads its exports map, and loads the ESM build.

// supabase/functions/orders/index.ts
import { Logger } from 'npm:@partner-api/logger@^3.0.0';

const logger = new Logger({
  tenantToken: Deno.env.get('PARTNER_API_TENANT_TOKEN')!,
  baseUrl: 'https://ingest.partnerapi.com',
  // The isolate dies with the Response, so never wait on a timer for a drain.
  flushIntervalMs: 0,
});

const apiKey = Deno.env.get('PARTNER_API_APP_KEY')!;

Deno.serve(async (req) => {
  const start = Date.now();
  const { pathname } = new URL(req.url);

  const correlationId = await logger.logRequest(apiKey, {
    method: req.method,
    path: pathname,
  });

  const order = await createOrder(req);

  logger.logResponse(apiKey, {
    statusCode: 200,
    duration: Date.now() - start,
    correlationId,
  });

  await logger.flush(); // see "Flushing" below
  return Response.json(order);
});

Both credentials belong in Supabase secrets, not in the function source:

supabase secrets set PARTNER_API_TENANT_TOKEN=tenant_live_xxxxxxxxxxxx
supabase secrets set PARTNER_API_APP_KEY=your-app-api-key

Secrets are readable from Deno.env.get immediately — no redeploy. Locally the same two names come from supabase/functions/.env, which the CLI loads for you; any other filename has to be passed as --env-file. Pick names outside Supabase's reserved SUPABASE_* / SB_* set.

To drop the specifier prefix from function code, map it once in supabase/functions/deno.json; Supabase rejects bare specifiers that aren't mapped or prefixed with npm: / jsr:.

{
  "imports": {
    "@partner-api/logger": "npm:@partner-api/logger@^3.0.0"
  }
}

Flushing: the isolate dies with the Response

The Edge runtime tears the isolate down as soon as your handler's Response is returned, so nothing left in the buffer at that moment is ever sent. Call await logger.flush() before returning, as in the quickstart above — one batched POST for the whole request, and it never rejects.

Where the runtime offers waitUntil, hand it the flush instead and keep even that round-trip off the response path. EdgeRuntime is a runtime global rather than something on the request context, and is absent under a bare deno run or a unit test around the handler, so feature-detect it:

type EdgeRuntimeGlobal = {
  EdgeRuntime?: { waitUntil(promise: Promise<unknown>): void };
};

/** Drains past the Response where the runtime allows it; awaitable where it doesn't. */
function drain(): Promise<void> {
  const runtime = (globalThis as EdgeRuntimeGlobal).EdgeRuntime;
  if (runtime?.waitUntil) {
    runtime.waitUntil(logger.flush());
    return Promise.resolve(); // the instance stays up until the flush resolves
  }
  return logger.flush();
}

Locally, feature detection alone is not enough: the CLI runtime does define EdgeRuntime, but defaults to policy = "oneshot", which terminates the instance once the request completes and cuts the deferred flush off. Switch the policy so background work survives the response:

# supabase/config.toml
[edge_runtime]
policy = "per_worker"

Custom transport

fetch can be overridden — useful for proxying, retries, instrumentation, or capturing requests in tests:

const logger = new Logger({
  tenantToken,
  fetch: (url, init) => myInstrumentedFetch(url, init),
});

Upgrading from v2

v3 makes delivery buffered and non-throwing (PAPI-3647). Every signature is unchanged and type-compatible — info still returns Promise<void>, logRequest still returns Promise<string> — so existing code compiles and runs untouched. What changes is what those promises mean.

Breaking changes:

  1. Log methods no longer reject. info / warn / error / debug / logRequest / logResponse return an already-resolved promise; a try/catch or .catch() around them now never fires. Delivery failures arrive at onError instead. If you were counting failed sends at the call site, move that counter into the hook.
  2. await no longer means "delivered". It means "buffered". Add await logger.flush() at the point where you need the entries to have landed — before returning a response on a suspending runtime, or on a shutdown path. This is what the old "await every call" serverless caveat collapses into.
  3. One POST now carries many entries. Batches are grouped by API key, level, partnerId and upstream attribution, and capped at the ingest limit of 1000 entries. A test asserting one HTTP request per log line needs updating; the per-entry wire format is unchanged.
  4. Failed batches are retried (network faults, 408, 429, 5xx; up to maxRetries, default 3) and dropped after that. Set maxRetries: 0 for the old one-attempt behaviour.
  5. metric / metrics are unchanged — still awaited, still throwing.

Not breaking, worth knowing: flushIntervalMs defaults to 2000ms and the timer is unref'd, so it never keeps a Node process alive. On a runtime that freezes between requests, set flushIntervalMs: 0 and drive drains from flush().

Upgrading from v1

v2 is a runtime-portability release. Logger, setContext, info/warn/error/debug, logRequest, logResponse, metric, metrics and redactPII all keep their v1 signatures, and the request body is byte-identical to v1 — verified by diffing the raw bytes of both versions across logs, metrics and full-context payloads.

Breaking changes:

  1. Proxy environment variables are no longer honoured. The big one — see Egress proxies need a transport. If you send logs through a proxy, v2 needs a fetch override or it silently stops delivering.
  2. axios is gone. It was the package's only dependency, and it is no longer installed transitively — anything that relied on that needs its own axios dependency.
  3. Deep imports are no longer resolvable. The package now declares an exports map, so only @partner-api/logger is importable; reaching into @partner-api/logger/dist/... breaks. Everything public is exported from the root.
  4. engines: node >=20 is declared where v1 declared nothing. Under engine-strict this blocks installs on Node 18, which does otherwise work.
  5. Network-failure messages differ. Status-code failures are unchanged (Failed to send log: Request failed with status code 500). Connection failures now read as the underlying fault the same way axios reported it (Failed to send log: connect ECONNREFUSED 127.0.0.1:3003), unwrapped out of fetch's opaque TypeError: fetch failed, with the full chain kept on error.cause. Exact strings come from the runtime, so don't match on them.
  6. Request headers differ. No more User-Agent: axios/1.x; the runtime sets its own defaults. Only relevant if a WAF or analytics keys off them.

Apart from the proxy case, no code change is needed for callers already on the documented API.

Outbound traffic & upstream attribution (PAPI-687)

By default every log entry is treated as inbound (partner → tenant). To flag traffic the partner makes outbound to an upstream integration, set the direction context plus the upstream-attribution fields:

logger.setContext({
  direction: 'outbound',
  upstreamIntegration: 'stripe', // name or ID, preferred for attribution
  upstreamBaseUrl: 'https://api.stripe.com', // fallback when no name is known
});

await logger.info(apiKey, 'Calling Stripe charges API', {
  method: 'POST',
  path: '/v1/charges',
});
  • direction is shipped as a Loki stream label so queries can isolate one direction: {tenantId="…", direction="outbound"}. Allowed values are inbound (default) and outbound; ingest rejects anything else with a 400.
  • upstreamIntegration / upstreamBaseUrl are folded into the log line as the structured fields upstream_integration / upstream_base_url (queryable via | json | upstream_integration="stripe"). They are not stream labels — base URLs are high-cardinality and would explode Loki's series count.
  • Backwards compatible: omitting direction leaves entries on the existing inbound streams unchanged; no migration is needed for historical data.

PII Redaction Helper

For call sites that need to redact PII before passing user data into a downstream system whose logs you don't control, the package exposes a public redactPII() helper. The ruleset mirrors what the ingest service applies internally — emails, JWTs, Bearer tokens, named API-key prefixes, passwords, phone numbers, credit cards, IPv4/IPv6 addresses, and URL query strings.

import { redactPII } from '@partner-api/logger';

// Strings (regex pass)
redactPII('contact [email protected]');
// → 'contact [EMAIL_REDACTED]'

redactPII('Authorization: Bearer abc.def.ghi');
// → 'Authorization: Bearer [TOKEN_REDACTED]'

// Headers (sensitive keys replaced wholesale)
redactPII({
  Authorization: 'Bearer secret',
  'X-Api-Key': 'sk-livetestkey1234567890',
  Cookie: 'session=abc',
});
// → {
//     Authorization: 'Bearer [TOKEN_REDACTED]',
//     'X-Api-Key': '[KEY_REDACTED]',
//     Cookie: '[COOKIE_REDACTED]',
//   }

// JSON body / deeply nested objects
redactPII({
  user: {
    email: '[email protected]',
    credentials: { password: 'hunter2', refresh_token: 'rt_xyz' },
  },
});
// → {
//     user: {
//       email: '[EMAIL_REDACTED]',
//       credentials: {
//         password: '[PASSWORD_REDACTED]',
//         refresh_token: '[TOKEN_REDACTED]',
//       },
//     },
//   }

// Query strings (encoded as a Record)
redactPII({ user: 'alice', api_key: 'sk-livetestkey1234567890' });
// → { user: 'alice', api_key: '[KEY_REDACTED]' }

Options (all default to true except redactUuids):

redactPII(input, {
  preserveStructure: true, // keep sensitive keys with a redacted placeholder; false drops them
  redactEmails: true,
  redactApiKeys: true,
  redactTokens: true,
  redactPasswords: true,
  redactPhoneNumbers: true,
  redactCreditCards: true,
  redactIpAddresses: true,
  redactUrls: true,
  redactUuids: false, // off by default — opt in for log-line redaction
});

redactPII is pure — the input is never mutated. The return type matches the input type (string in → string out, object in → deep-cloned object out, null/undefined pass through).