Skip to content
DocspackagesDocumentation

@lunora/fingerprint

Zero-dependency error fingerprinting — one stable grouping hash that collapses noisy errors into Issues for Studio and the Cloud.

PackagesFingerprint

@lunora/fingerprint is Lunora's deterministic error-grouping core: it folds every occurrence of "the same" error onto one stable 16-character hash (an Issue), no matter where the error was observed. A live observability sink event and a request-log row persisted hours earlier compute the same hash, so the local Studio Issues panel and a Lunora Cloud incident describe the same object.

The core algorithm is vendored from Superlog Labs' @superlog/fingerprint (Apache-2.0, see the package NOTICE), with the Node-only crypto backend replaced by a portable synchronous SHA-256. The package is zero-dependency and runs unchanged in the browser (Studio), the Cloudflare Workers (workerd) runtime, and Node.

import { fingerprintError } from "@lunora/fingerprint";

const fp = fingerprintError({
    functionPath: "messages:list",
    message: "User 12345 not found",
    code: "NOT_FOUND", // metadata only — never part of the hash
});
// → { hash: "168d714cba85f1c8", title: "User 12345 not found",
//     culprit: "messages:list", bucket: "user <n> not found", code: "NOT_FOUND" }

The hash contract

fingerprintError({ functionPath, message, code? }) hashes over exactly

sha256("lunora::" + functionPath + "::" + bucket(message))

truncated to 16 hex characters. Two things follow from that:

  • functionPath + normalized message is the whole identity. That pair is the one thing every Lunora error source can supply: an in-flight observability event carries { code, message, status }, while the durable request log (__lunora_reqlog__) persists only outcome + error_message, with no stored code. Hashing anything more would split a live Issue from one recomputed from history.
  • code is display metadata, never hashed. It rides along on the result (fp.code) for badges and filtering, but changing it does not create a new Issue.

The result also carries a title (first message line, capped at 120 chars), the culprit (the function path, or container:<name> for container lifecycle errors, which flow through the same seam), and the bucket the hash was built from, which is what you inspect when two errors did or didn't group.

fingerprintError is pure and synchronous, so it is safe to call per row when grouping a request-log page client-side.

The message bucketer

Raw messages embed per-occurrence noise: a route scanner probing 5,000 paths or a per-user id in the message must not explode one bug into 5,000 Issues. messageBucketFor(message) normalizes the message before hashing by stripping/tokenizing:

  • URLs and request paths
  • UUIDs, long ids, and hex blobs
  • IP addresses and timestamps
  • bare numbers ("User 12345 not found""user <n> not found")

so a sweep of GET /wp-admin/... 404s folds into a single Issue. The bucketer (and normalizeMessage, its heavier sibling) are exported for inspection and tests.

Stack-aware variants

For OTLP-sourced telemetry that carries a real exception.stacktrace, the stack-aware fingerprint / fingerprintLog exports add top-5 user-frame normalization on top of the message bucket, the vendored superlog behavior. Inside a Lunora deployment you almost always want fingerprintError: Lunora errors carry no stack frames on the wire.

Where it feeds

  • @lunora/do computes fingerprints over the bounded request-log readout behind the getIssues admin RPC.
  • Studio renders the grouped result in the Issues panel: one row per hash, with occurrence counts, first/last seen, and the code badge.
  • Lunora Cloud computes the same hash over durable OTLP telemetry, so a local Issue and a cloud incident share an identity.

You normally don't install this package directly; it arrives as a dependency of @lunora/do and the Studio. Reach for it yourself only to group errors in your own tooling with the same identity Lunora uses.