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

@muhammetgoktug/mg-logger

v0.6.0

Published

Self-hosted log, error & performance monitoring SDK. Works in Node.js, Next.js, React, React Native and browsers.

Readme

mg_logger SDK

Self-hosted log, error & performance monitoring SDK. Works in Node.js, Next.js, React, React Native and browsers.

  • Zero runtime deps (no zod, no axios — uses native fetch).
  • Tree-shakeable subpath exports per platform.
  • Dual ESM/CJS build with TypeScript declarations.
  • Tiny: each platform entry ships only what it needs.

Publish to npm (maintainer cheatsheet)

Quick reference for shipping a new version. Run from the sdk/ directory.

cd mg_logger_sdk
npm whoami
npm install
# Bump the version. NOTE: also bump SDK_INFO.version in the 5 entry files below
# to match — `npm version` only touches package.json. Skip this step if you've
# already set the version manually in all 6 places.
npm version patch   # 0.6.0 → 0.6.1  (bug fixes)
# npm version minor   # 0.6.0 → 0.7.0  (new feature, backwards compatible)
# npm version major   # 0.6.0 → 1.0.0  (breaking change)
npm run build       # tsup → dist/ (also runs automatically via prepublishOnly)
npm publish --access public   # NOT `npm run publish` — there is no such script
git push --follow-tags

The 5 entry files whose SDK_INFO.version must match package.json: core/client.ts, node/index.ts, nextjs/server.ts, nextjs/client.ts, react-native.ts. They are currently all at 0.6.0.

Install

npm install @muhammetgoktug/mg-logger
# or
pnpm add @muhammetgoktug/mg-logger
# or
yarn add @muhammetgoktug/mg-logger

DSN format

<protocol>://<publicKey>@<host>[:port]/<projectId>

Example: https://[email protected]/4

The SDK posts envelopes to ${origin}/api/${projectId}/envelope.


Same-origin proxy (no CORS)

If your ingest server doesn't expose CORS headers (deliberate hardening mirroring Sentry's design), point the browser at a same-origin path that your Next.js / Express / Nginx server forwards to the real ingest. No preflight ever fires.

next.config.ts:

async rewrites() {
  return [
    { source: "/_ek/:path*", destination: "https://ingest.example.com/api/:path*" },
  ];
}

instrumentation-client.ts:

import { initBrowser } from "@muhammetgoktug/mg-logger/nextjs/client";

initBrowser({
  dsn: process.env.NEXT_PUBLIC_MG_LOGGER_DSN!,
  sameOriginProxy: "/_ek",   // SDK posts to /_ek/<projectId>/envelope
});

The SDK parses the project id from the DSN and builds /_ek/<projectId>/envelope automatically. Server-side init() still uses the absolute DSN host (no CORS concern there).

For full control, pass ingestUrl directly — wins over sameOriginProxy.


Scope attributes

setAttribute(key, value) rides on every log.*, captureHttp, and span event emitted afterwards — handy for per-route correlation ids:

import { setAttribute, log, captureHttp } from "@muhammetgoktug/mg-logger";

// e.g. in a RouteViewLogger:
setAttribute("view_id", crypto.randomUUID());
log.info("page.view", { path: "/dashboard" });
// later, every API call's captureHttp() carries the same view_id automatically
captureHttp({ method: "GET", url: "/api/me", status: 200, duration_ms: 42 });

Available APIs: setAttribute, removeAttribute, clearAttributes.

(Scope tags via setTag still flow only into captureException / captureMessage.)


Node.js

import { init, captureException, log } from "@muhammetgoktug/mg-logger/node";

init({
  dsn: process.env.MG_LOGGER_DSN!,
  environment: process.env.NODE_ENV,
  release: process.env.GIT_SHA,
  // installs uncaughtException + unhandledRejection handlers by default
});

try {
  doWork();
} catch (err) {
  captureException(err);
}

log.info("worker started", { workerId: 1 });

Optional Express-shaped middleware (no express runtime dependency — typed structurally):

import express from "express";
import { tracingHandler, errorHandler } from "@muhammetgoktug/mg-logger/node";

const app = express();
app.use(tracingHandler()); // before routes — emits APM spans
// ... your routes ...
app.use(errorHandler());   // after routes — captures thrown errors

Next.js (App Router)

Server

Create instrumentation.ts in the project root:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { init } = await import("@muhammetgoktug/mg-logger/nextjs");
    init({
      dsn: process.env.MG_LOGGER_DSN!,
      environment: process.env.NODE_ENV,
      release: process.env.GIT_SHA,
    });
  }
}

// Next 15+: forward route errors to mg_logger
export { captureRequestError as onRequestError } from "@muhammetgoktug/mg-logger/nextjs";

Client

// app/providers.tsx
"use client";
import { useEffect } from "react";
import { initBrowser } from "@muhammetgoktug/mg-logger/nextjs/client";

export function Providers({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    initBrowser({
      dsn: process.env.NEXT_PUBLIC_MG_LOGGER_DSN!,
      environment: process.env.NODE_ENV,
      // Both default to on — shown here for visibility.
      autoInstrument: true,   // fetch + xhr + console + navigation breadcrumbs
      captureContext: true,   // browser/os/viewport context on every event
    });
  }, []);
  return <>{children}</>;
}

Automatic breadcrumbs & context (browser)

initBrowser installs automatic breadcrumbs and captures device context out of the box. Everything is fail-soft (a broken hook never breaks your page) and network breadcrumbs skip your own ingest endpoint to avoid feedback loops.

initBrowser({
  dsn,
  autoInstrument: {
    fetch: true,        // default on
    xhr: true,          // default on
    console: true,      // console.error / console.warn → breadcrumbs (default on)
    navigation: true,   // SPA route changes (default on)
    domClicks: false,   // click selectors — OFF by default (can leak PII)
  },
  captureContext: true, // browser name/version, OS, viewport, locale
});

Pass autoInstrument: false to disable all of them, or captureContext: false to skip context. Use setContext("key", {...}) to attach your own context group, and beforeSend to redact sensitive breadcrumb/URL data.

The auto-instrumentation code lives only in the nextjs/client (and react-native) bundles — the universal index entry stays free of it, so the core import is unchanged and tree-shakeable.


React

import { ErrorBoundary } from "@muhammetgoktug/mg-logger/react";

export default function App() {
  return (
    <ErrorBoundary fallback={<p>Something broke.</p>}>
      <Routes />
    </ErrorBoundary>
  );
}

React Native

import { initReactNative, captureException } from "@muhammetgoktug/mg-logger/react-native";

initReactNative({
  dsn: "https://your-key@your-host/project-id",
  environment: __DEV__ ? "development" : "production",
  appVersion: "1.2.3",   // surfaced under the `app` context (RN can't read it alone)
  build: "456",
});

// hooks into RN's ErrorUtils for uncaught JS fatals; manual capture also works:
try { ... } catch (e) { captureException(e); }

initReactNative automatically: hooks ErrorUtils (fatal/non-fatal JS errors), tracks unhandled promise rejections, installs network + console breadcrumbs, captures device/OS context, and persists fatal/error events offline so a JS crash that happens before the next flush is replayed on the next launch.

initReactNative({
  dsn,
  autoInstrument: { fetch: true, xhr: true, console: true }, // all default on
  captureContext: true,        // device/os context (default on)
  enableStallDetector: true,   // JS-thread stall ("ANR-ish") heartbeat (default off)
  stallThresholdMs: 5000,
  // persistence: "auto" (default on RN) — AsyncStorage if installed, else no-op
});

The stall detector is JS-only: it flags long synchronous JS work, but it is not a true native ANR (see native module below). Offline persistence uses the optional peer @react-native-async-storage/async-storage — install it to enable durable replay; without it the SDK degrades silently.

Navigation breadcrumbs (React Navigation)

No dependency on react-navigation — wire the hook yourself:

import { createNavigationBreadcrumbHook } from "@muhammetgoktug/mg-logger/react-native";

const onStateChange = createNavigationBreadcrumbHook(client);
<NavigationContainer onStateChange={onStateChange}>{...}</NavigationContainer>

Native crashes & ANR (opt-in)

The default RN entry catches JS-level errors only. To also capture native crashes (iOS signals/NSException, Android JVM crashes) and ANRs, use the react-native/native subpath. This ships native code, so it requires autolinking + pod install (and is autolinked for Expo via the bundled config plugin):

import { initReactNativeWithNative } from "@muhammetgoktug/mg-logger/react-native/native";

initReactNativeWithNative({ dsn, appVersion: "1.2.3" });

Native handlers write a crash report to disk at crash time (when JS/network can't run) and the report is uploaded on the next launch. Handlers chain to any previously-installed crash reporter (e.g. Crashlytics). Native stacks are stored raw for now (native symbolication via dSYM/ProGuard mapping is a later phase). Expo apps add the plugin in app.json:

{ "expo": { "plugins": ["@muhammetgoktug/mg-logger"] } }

HTTP body capture

captureHttp({ method, url, status, duration_ms, request_body, response_body }) records a request/response pair as a structured log. Bodies are truncated to maxHttpBodyBytes (default 256 KB) — when truncation happens the SDK sets sibling attributes so the viewer can still pretty-print the prefix:

attributes.response_body              // prefix, no in-band marker
attributes.response_body_truncated    // "true"
attributes.response_body_original_size// "358400"

Tune via init({ maxHttpBodyBytes, maxEnvelopeBytes }):

| Option | Default | Notes | |---|---|---| | maxHttpBodyBytes | 256 * 1024 | Per-body cap. 0 disables (unsafe). | | maxEnvelopeBytes | 800 * 1024 | Transport splits a batched envelope into multiple POSTs when serialized size exceeds this. Keeps a margin under the server's 4 MB body limit. |

A single oversize item is still sent on its own; the server's hard cap and LOG_BODY_HARD_CAP_BYTES (default 1 MB) become the final gate.


Source maps (de-minify stack traces)

Web bundles (and Hermes release builds) ship minified, so stack traces arrive with names like t in bundle.js. Upload your source maps per release and the ingest server resolves frames to your original code before grouping, so issues group on real names and the panel shows source context lines.

Run the bundled CLI in CI after each build:

npx mg-logger-sourcemaps \
  --url https://ingest.example.com \
  --key <public-key> \
  --project 42 \
  --release v1.2.3 \
  ./dist

It uploads every .map under the given path, keyed by (project, release). Re-uploading the same release overwrites it. Maps are stored server-side (GridFS); the panel's Source maps page lists and deletes them, and old releases are swept automatically (SOURCEMAP_KEEP_RELEASES, default 10).

Set the event's release (via init({ release })) to the same value you pass to --release, or frames can't be matched. Hermes: compose and upload the .hbc.map for the release. The CLI is dependency-free; map parsing happens only on the server, so the SDK stays zero-dep.


Public API (summary)

All entry points re-export the same universal helpers from ./core/singleton:

| Function | Description | |---|---| | init(options) | Create the global client (platform entries add their own handlers on top). | | captureException(err, hint?) | Send an error event. | | captureMessage(msg, level?) | Send a message event. | | addBreadcrumb(crumb) | Attach a breadcrumb to subsequent events. | | setUser(user) / setTag(k,v) / setExtra(k,v) | Mutate the global scope. | | setContext(key, value) | Attach a device/os/app/custom context group. | | flush() | Force-send the buffered envelope. | | close() | Stop the timer and flush. | | log.info / .warn / .error / .debug / .fatal | Structured logger. | | initBrowser(options) | (nextjs/client) Browser variant of init; installs auto-breadcrumbs + context. | | initReactNative(options) | (react-native) RN variant; auto-breadcrumbs, context, offline queue. | | initReactNativeWithNative(options) | (react-native/native) RN + native crash/ANR capture (opt-in, needs native build). | | createNavigationBreadcrumbHook(client) | (react-native) React Navigation onStateChange breadcrumb hook. | | captureRequestError(...) | (nextjs) Map to Next 15's onRequestError. |

Pure helpers also exported from the root: parseDsn, parseStackString, parseStackTrace.

Most platform inits also accept persistence ("auto" | false | a custom PersistentStore) — durable storage of unsent fatal/error events so a hard crash replays them on the next launch. Default: on for browser/RN, off for node.


Raw HTTP fallback (no SDK)

Useful for edge runtimes, build environments, or anywhere the SDK is too heavy. Just POST JSON to the /store endpoint:

// store-only payload — server normalizes into the envelope schema.
async function captureErrorRaw(err: Error, dsn: string) {
  const u = new URL(dsn);
  const publicKey = u.username;
  const projectId = u.pathname.replace(/^\//, "");
  await fetch(`${u.origin}/api/${projectId}/store`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-ek-public-key": publicKey,
    },
    body: JSON.stringify({
      type: "error",
      level: "error",
      message: err.message,
      error_type: err.name,
      stack: err.stack,
    }),
    keepalive: true, // browser: survive page unload
  });
}

Curl equivalent:

curl -X POST "https://sentry.example.com/api/4/store" \
  -H "content-type: application/json" \
  -H "x-ek-public-key: YOUR_PUBLIC_KEY" \
  -d '{"type":"log","level":"info","message":"hello from curl"}'

The /store endpoint accepts a single event; /envelope accepts a batch.


Next.js proxy pattern (avoid browser CORS)

Browsers sending events directly to a different origin (sentry.example.com) trigger a CORS preflight. To skip it, proxy through your own Next.js app at /_ek/* and point the browser's DSN at the same origin.

next.config.ts

import type { NextConfig } from "next";

// Derive the mg_logger ingest origin from the DSN so the rewrite has one source
// of truth. Browser sends events to /_ek/* (same-origin → no CORS preflight);
// Next forwards the request server-side to the real ingest host.
const ingestOrigin = process.env.NEXT_PUBLIC_MG_LOGGER_DSN
  ? new URL(process.env.NEXT_PUBLIC_MG_LOGGER_DSN).origin
  : "";

const nextConfig: NextConfig = {
  async rewrites() {
    if (!ingestOrigin) return [];
    return [
      { source: "/_ek/:path*", destination: `${ingestOrigin}/api/:path*` },
    ];
  },
};

export default nextConfig;

Same-origin DSN trick in the client wrapper

// src/lib/eksentry.ts — direct HTTP wrapper (no SDK), same-origin in the browser.
type Endpoint = { ingestUrl: string; publicKey: string } | null;

function parseDsn(dsn: string | undefined): Endpoint {
  if (!dsn) return null;
  try {
    const u = new URL(dsn);
    const publicKey = u.username;
    const projectId = u.pathname.replace(/^\//, "");
    if (!publicKey || !projectId) return null;
    // Browser → same-origin proxy path; Server → absolute origin.
    const ingestUrl =
      typeof window === "undefined"
        ? `${u.origin}/api/${projectId}/store`
        : `/_ek/${projectId}/store`;
    return { ingestUrl, publicKey };
  } catch {
    return null;
  }
}

The same trick works for the SDK: if you build your DSN as https://[email protected]/PROJECT_ID in the browser, the SDK will hit https://your-app.com/api/PROJECT_ID/envelope — same origin → no preflight — and your Next rewrite forwards it to the real ingest.


License

MIT © Muhammet Goktug. See LICENSE.