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

@middleware.io/otel-extensions

v0.5.0

Published

Middleware OpenTelemetry extensions: exception.stack_details enrichment and vcs.* resource detection, using only public OpenTelemetry extension points.

Downloads

751

Readme

@middleware.io/otel-extensions

Project state, coverage matrix and open work: see CLAUDE.md.

Middleware's OpenTelemetry additions, implemented entirely against public OpenTelemetry extension points — no SDK fork, no patching, no overrides. Zero runtime dependencies; TypeScript sources with bundled type definitions.

The OpenTelemetry packages are peerDependencies and are referenced only as types, so nothing from them is pulled in or bundled at runtime.

const {
  vcsDetector,
  ExceptionStackDetailsSpanProcessor,
} = require('@middleware.io/otel-extensions');

const sdk = new NodeSDK({
  resourceDetectors: [envDetector, hostDetector, processDetector, vcsDetector],
  spanProcessors: [new ExceptionStackDetailsSpanProcessor(), ...yourExporters],
});

vcsDetector

A standard ResourceDetector. Adds:

  • vcs.commit_sha — from MW_VCS_COMMIT_SHA, else HEAD resolved out of the local .git directory
  • vcs.repository_url — from MW_VCS_REPOSITORY_URL, else remote.origin.url from .git/config (with any .git suffix stripped)

The .git reading is done by parsing the files directly (src/git-metadata.ts) — no git binary and no git library dependency. It handles ref indirection, packed-refs, and the quoting/comment rules of git-config. Resolution is cached at module scope, so the lookup runs once per process no matter how many providers detect it.

Production images usually don't ship .git, so setting the two environment variables from CI is the realistic path; the .git fallback mainly helps in dev and staging.

ExceptionStackDetailsSpanProcessor

Adds exception.stack_details to recorded exception events — an expanded form of exception.stacktrace. For each resolvable frame:

| key | | |---|---| | exception.file | absolute path | | exception.line / exception.column_number | position | | exception.function_name | or anonymous | | exception.is_file_external | whether the frame is under node_modules | | exception.function_body | source of the enclosing function — own code only | | exception.start_line / exception.end_line | 1-based, inclusive file lines of that body | | exception.function_truncated | present when the function exceeded maxFunctionLines |

exception.language matches the key the Python SDK emits, and becomes typescript when a map resolves back to TypeScript sources.

Compiled and bundled services

For a TypeScript or bundled service, the file that runs isn't the file anyone wrote: line numbers are shifted, types are erased, and syntax has been downlevelled. dist/server.js:33 might be src/server.ts:48, with order.status ?? OrderStatus.Pending showing up as a three-way ternary over a hoisted var _a.

When a source map yields both the original position and its text, the standard keys above are populated from the original source, and the compiled position is preserved alongside:

| key | | |---|---| | exception.generated_file | the compiled file the frame was reported in | | exception.generated_line / exception.generated_column_number | 1-based | | exception.generated_function_body | enclosing function in the compiled file | | exception.generated_start_line / exception.generated_end_line | 1-based, inclusive |

So a consumer that only knows the standard keys shows source instead of build output, with no change on its side.

How much is recoverable depends on the build, and degrades in tiers:

  1. map with sourcesContent (esbuild and webpack defaults; tsc with inlineSources) → original position and text, so the keys are promoted
  2. map without it, but the original files are still in the image (any single-stage Docker build) → read off disk, same result
  3. map with neither → the position is reported as exception.original_file / exception.original_line, but not promoted. function_body has to stay consistent with file and start_line, and promoting a position whose text we don't have would mislabel every line of the body
  4. no map → nothing added

Tier 3 is the common tsc case, since sourceMap: true does not imply sourcesContent — that needs inlineSources: true, a one-line change that moves a build from tier 3 to tier 1.

Source maps are read only for frames whose source is captured, so the cost stays proportional to the value, and decoded maps are cached per file. Set resolveSourceMaps: false to skip it entirely.

What gets a body

Source is captured for the application's own frames only. Dependency frames (node_modules) carry location alone: their bodies are rarely actionable and, being the bulk of a typical stack, they crowd out the code a reader can act on. Set includeExternalSource: true to capture them anyway.

function_body is the enclosing function — signature through closing brace — not a fixed window, so the reader sees the parameters and guard clauses rather than an arbitrary slice. Detection is brace matching (string- and comment-aware), not parsing; when it can't find a function, or the function is longer than maxFunctionLines, it falls back to contextLines either side of the throw site and sets exception.function_truncated.

Mapping the body back to the file

start_line/end_line are 1-based and inclusive, and the body is always contiguous, so:

file line N  ===  body line (N - start_line)

The throwing line is exception.line, so it sits at body[exception.line - start_line]. (Before 0.3.0 start_line was a 0-based slice index reported as a line number, so every line mapped one row off.)

Frames that don't resolve to a real file on disk (node:internal/..., <anonymous>) are skipped, so an error thrown entirely inside Node core produces no stack_details at all.

Set MW_RECORD_EXCEPTION_SOURCE=false to keep the frame metadata but skip reading source files from disk.

Supported OpenTelemetry versions, and onEnding vs onEnd

Works on OpenTelemetry 1.26+ and 2.x. The OTel packages are optional, type-only peerDependencies, so buildExceptionStackDetails can also be used on its own with none of them installed.

Enrichment has to land before an exporting processor serializes the span, and the available hook differs by version — so it is attempted from both, and is idempotent:

  • 2.x calls every processor's onEnding before any onEnd, so enrichment is independent of registration order. The later onEnd finds the work done and returns.

  • 1.x has no onEnding at all — it is absent from MultiSpanProcessor and from the SpanProcessor interface — so onEnd is the only hook. That path is order-dependent: register this processor ahead of the exporting one, or the span is serialized before enrichment runs and the attribute is silently lost. NodeSDK registers spanProcessors in array order, so listing this one first is enough:

    spanProcessors: [
      new ExceptionStackDetailsSpanProcessor(),
      new BatchSpanProcessor(exporter),
    ]

Both paths are covered by tests that run against a real 1.26 SDK installed under an npm alias, including one asserting the ordering hazard above.

Worth knowing when testing this yourself: an in-memory exporter holds spans by reference and will make a late-mutation bug look like it works.

Options

Attributes written from a processor bypass the span's attributeValueLengthLimit, so this caps itself. All options are set per instance:

new ExceptionStackDetailsSpanProcessor({
  maxFrames: 20,               // frames captured
  maxAttributeLength: 131072,  // 128 KiB serialized
  maxFunctionLines: 80,        // largest function captured whole
  contextLines: 10,            // window either side when falling back
  includeExternalSource: false, // capture node_modules bodies too
  resolveSourceMaps: true      // map generated positions back to source
});

When the serialized value exceeds the budget, the deepest frames are dropped first — the throw site is frame 0, so the most useful context is kept.

Caveat

The processor parses the recorded exception.stacktrace string, not the live Error. If OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT is set, that string is already truncated and tail frames are lost. The default is unlimited.

Development

npm install
npm run compile     # tsc -> build/
npm test            # mocha + ts-node against src/
npm run typecheck   # type-checks src/ and test/ together

prepublishOnly runs clean + compile + typecheck + test, so a publish cannot ship a build that doesn't compile or pass its tests.

Node-only by design: it reads the .git directory and application source off disk, neither of which a browser can do. Browser no-op stubs exist on the fork-approach-archive branch if a RUM build ever needs them.

Releasing

.github/workflows/ holds the automation. It does nothing while this directory lives inside the opentelemetry-js fork — GitHub only reads workflows from a repository's root, and the fork's root belongs to upstream (21 workflow files, which we deliberately don't touch so rebases stay clean). The moment this directory becomes its own repository, both workflows go live with no changes.

  • ci.yml — every push to master and every PR: compile, typecheck and test on Node 18/20/22, plus a single-arch image build that is loaded but not pushed, asserting the bundle still contains the extensions package and that no Middleware code has leaked into @opentelemetry.

  • release.yml — triggered by a v* tag:

    npm version minor -m 'release %s'
    git push origin master --follow-tags

    It refuses to release if the tag and package.json version disagree, publishes to npm (gated by prepublishOnly), waits for the registry to actually serve the new version, pins the init container to that exact version, builds and pushes the multi-arch image, and then runs the pushed image on both architectures to confirm it really contains the released package.

Two details worth knowing, both learned the hard way:

  • npmjs.org's public read path lags a successful publish — sometimes by minutes. The image build is a plain npm install, so it will 404 on a version that definitely exists. Hence the wait-for-registry step.
  • The image is pushed straight from buildx. A docker push of a locally loaded manifest list can flatten it to a single architecture.

Secrets required: NPM_TOKEN. For GHCR, the default GITHUB_TOKEN may not have write access because the image sits under a different repository's package path (opentelemetry-operator/autoinstrumentation-node) — either grant this repo write on that package, or set a GHCR_TOKEN PAT.

To rebuild or retag an image without republishing to npm, run the workflow manually from the Actions tab and pass a version plus an optional extra image tag (e.g. 0.64b0-opsai-record-exception-v4).