Docs

    Send us your errors

    Monitoring from outside tells you the site is down. This tells you why it answered 500. There is nothing to install: one environment variable, one small file, and your crashes arrive grouped by cause with the stack, the release and the deploy that preceded them.

    The whole protocol

    Create a project in Errors and copy its DSN. It is an address and a write-only key in one string:

    SERVERINSPECTOR_DSN=https://YOUR_KEY@api.serverinspector.com/YOUR_PROJECT
                            └key┘ └───── host ─────┘ └project┘

    POST a crash to it. Two fields are required, the rest help:

    curl -sS -X POST "https://api.serverinspector.com/api/errors/YOUR_PROJECT" \
      -H "X-ServerInspector-Key: YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "error": { "type": "TypeError", "message": "cart.total is not a function", "stack": "at checkout (/app/src/checkout.js:42:11)" },
        "release": "v3.1.0",
        "environment": "production",
        "url": "https://example.com/checkout"
      }'

    Send the stack exactly as your language produced it. We parse the JavaScript, Ruby and Python formats and work out which frames are yours, which is what names the cause on the issue page.

    Let your AI do it

    Copy this into Lovable, Bolt, Cursor, Claude Code or whatever builds your app, with your own DSN in place of the placeholder. It says what to write, what never to send, and how to prove it worked.

    Add error reporting to my application (this application, whatever language and framework it is written in). Report crashes to ServerInspector.
    
    ENDPOINT AND CREDENTIALS
    The DSN is: https://YOUR_KEY@api.serverinspector.com/YOUR_PROJECT
    It has the form https://KEY@HOST/PROJECT. From it, build:
      URL:    https://HOST/api/errors/PROJECT
      Header: X-ServerInspector-Key: KEY
    Read the DSN from an environment variable named SERVERINSPECTOR_DSN (add it to the env file and to the deployment config). Never hard-code it in source. In browser code the key is public by design, so it may be inlined at build time from the framework's public env prefix.
    
    WHAT TO SEND
    POST JSON to that URL with this body. Every field except error.type and error.message is optional:
    {
      "error": { "type": "TypeError", "message": "cart.total is not a function", "stack": "<the language's raw stack or backtrace as a string>" },
      "release": "<git sha or version, if available>",
      "environment": "<production | staging | development>",
      "url": "<the request URL or page URL>",
      "method": "<HTTP method, if a request>",
      "context": { "<small key>": "<small value>" }
    }
    The server parses V8, Firefox, Ruby and Python stack formats, so send the stack exactly as the language produced it. Do not invent a format.
    
    RULES
    1. Do NOT install any dependency or SDK. Write a small module of plain code (about 20 to 40 lines) using the language's built-in HTTP client.
    2. Never send passwords, tokens, API keys, cookies, authorization headers, full request bodies or personal data such as email addresses. Send only what is needed to find the bug.
    3. Reporting must never break the application: wrap the send in a try/catch (or equivalent), use a short timeout, and ignore any failure. Never await it in a request path if that would slow a response.
    4. Do not report in development or test runs unless SERVERINSPECTOR_DSN is set in that environment.
    5. De-duplicate: if the same message and stack are reported more than once within ten seconds, send it once.
    
    WHERE TO HOOK IT
    - Server: the framework's global exception handler or error middleware, plus process-level handlers (uncaughtException / unhandledRejection in Node, at_exit or a rescue in a base controller in Ruby, sys.excepthook or the framework's error signal in Python, set_exception_handler in PHP).
    - Browser: window.addEventListener("error") and window.addEventListener("unhandledrejection"), and the framework's error boundary if it has one. Use fetch with keepalive: true so a crash during navigation still reports.
    - Include the release: read the git sha from the build environment if one is exposed.
    
    THEN
    1. Add a route or a script that throws a test error on purpose, for example GET /debug/boom in a server or a button in a browser app, and guard it so it only exists when SERVERINSPECTOR_DSN is set.
    2. Trigger it once and confirm the POST returns HTTP 200.
    3. Remove nothing else; keep the change small and in one file where possible.
    4. Tell me the file you added, the environment variable to set, and how to trigger the test error.
    
    VERIFY WITHOUT CODE (for me, not for you to run in production):
    curl -sS -X POST "https://HOST/api/errors/PROJECT" -H "X-ServerInspector-Key: KEY" -H "Content-Type: application/json" -d '{"error":{"type":"HelloError","message":"it works"}}'

    The app's Errors page gives you the same prompt with your project's real DSN already in it.

    Or write it yourself

    Node.js, Express or plain

    // serverinspector.js  (no dependency)
    const DSN = process.env.SERVERINSPECTOR_DSN;
    const [, key, host, project] = /^https:\/\/([^@]+)@([^/]+)\/(\d+)$/.exec(DSN ?? "") ?? [];
    
    export async function reportError(error, extra = {}) {
      if (!DSN) return;
      try {
        await fetch(`https://${host}/api/errors/${project}`, {
          method: "POST",
          headers: { "Content-Type": "application/json", "X-ServerInspector-Key": key },
          body: JSON.stringify({
            error: { type: error?.name ?? "Error", message: String(error?.message ?? error), stack: error?.stack },
            release: process.env.GIT_SHA, environment: process.env.NODE_ENV, ...extra,
          }),
          signal: AbortSignal.timeout(3000),
        });
      } catch {} // reporting must never break the thing it reports on
    }
    
    process.on("uncaughtException", (e) => reportError(e));
    process.on("unhandledRejection", (e) => reportError(e));
    // Express: app.use((err, req, res, next) => { reportError(err, { url: req.originalUrl, method: req.method }); next(err); });

    Browser, one script tag

    <script
      src="https://serverinspector.com/e.js"
      data-dsn="https://YOUR_KEY@api.serverinspector.com/YOUR_PROJECT"
      data-release="v1.2.3"
      data-environment="production"
    ></script>
    
    <!-- Catches uncaught errors and unhandled promise rejections.
         Report one yourself: window.ServerInspector.report(err) -->

    Ruby on Rails

    # config/initializers/serverinspector.rb  (no gem)
    require "net/http"
    
    SERVERINSPECTOR_DSN = ENV["SERVERINSPECTOR_DSN"]
    
    def report_error(error, context = {})
      return if SERVERINSPECTOR_DSN.blank?
    
      key, host, project = SERVERINSPECTOR_DSN.match(%r{\Ahttps://([^@]+)@([^/]+)/(\d+)\z})&.captures
      uri = URI("https://#{host}/api/errors/#{project}")
      body = { error: { type: error.class.name, message: error.message, stack: error.backtrace&.join("\n") },
               release: ENV["GIT_SHA"], environment: Rails.env.to_s }.merge(context)
      Thread.new do
        Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json", "X-ServerInspector-Key" => key)
      rescue StandardError
        nil
      end
    end
    
    # Everything unhandled, in requests and in jobs:
    Rails.error.subscribe(Class.new do
      def report(error, handled:, severity:, context:, source: nil)
        report_error(error, url: context[:controller]&.request&.url) unless handled
      end
    end.new)

    Python, Django or Flask

    # serverinspector.py  (no package)
    import json, os, re, threading, urllib.request
    
    DSN = os.getenv("SERVERINSPECTOR_DSN", "")
    m = re.match(r"https://([^@]+)@([^/]+)/(\d+)", DSN)
    
    def report_error(exc, **extra):
        if not m:
            return
        key, host, project = m.groups()
        import traceback
        body = json.dumps({
            "error": {"type": type(exc).__name__, "message": str(exc), "stack": "".join(traceback.format_exception(exc))},
            "release": os.getenv("GIT_SHA"), "environment": os.getenv("ENV", "production"), **extra,
        }).encode()
        req = urllib.request.Request(f"https://{host}/api/errors/{project}", data=body,
                                     headers={"Content-Type": "application/json", "X-ServerInspector-Key": key})
        threading.Thread(target=lambda: urllib.request.urlopen(req, timeout=3), daemon=True).start()
    
    # Flask:   @app.errorhandler(Exception) -> report_error(e, url=request.url, method=request.method)
    # Django:  connect to got_request_exception, or call it in middleware

    PHP

    <?php // serverinspector.php  (no composer package)
    function report_error(Throwable $e, array $extra = []): void {
        $dsn = getenv('SERVERINSPECTOR_DSN');
        if (!$dsn || !preg_match('~^https://([^@]+)@([^/]+)/(\d+)$~', $dsn, $m)) return;
        $body = json_encode(array_merge([
            'error' => ['type' => get_class($e), 'message' => $e->getMessage(), 'stack' => $e->getTraceAsString()],
            'release' => getenv('GIT_SHA'), 'environment' => getenv('APP_ENV') ?: 'production',
        ], $extra));
        $ch = curl_init("https://{$m[2]}/api/errors/{$m[3]}");
        curl_setopt_array($ch, [CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $body, CURLOPT_TIMEOUT => 3,
            CURLOPT_RETURNTRANSFER => 1, CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-ServerInspector-Key: {$m[1]}"]]);
        curl_exec($ch); curl_close($ch);
    }
    set_exception_handler('report_error');

    The key, and who can use it

    Every project has its own generated key, and an event without a valid one is refused. What the key can do is deliberately tiny: create errors in that one project. It cannot read your errors, your monitors, your billing or anything else, so a leaked key is a nuisance and not a breach.

    • Server projects get a secret key. It is stored as a digest, shown once when it is minted, and lives in your environment. Nobody here can read it back, including us.
    • Browser projects get a public key, because a page cannot keep a secret: anyone who opens the developer tools sees it. That is true of every tool that reports from a browser.
    • Name your hosts. A project can list the sites allowed to report to it; anything from elsewhere is refused with a 403. This is the answer to "somebody copied my key out of my page".
    • Rotate any time. The new key works immediately and the old one stops the same second.
    • The plan's monthly allowance bounds the damage either way: past it we keep counting the cause and stop storing bodies.

    What we keep, and what we throw away

    • Cookies, Authorization headers, and any field whose name looks like a password, token, secret, key or card number are replaced with [scrubbed] before the event is written.
    • Email addresses inside values are scrubbed too.
    • A project can be set to "stack traces only": no request, no breadcrumbs, no tags.
    • Bodies are truncated (256 KB, 100 frames, 50 breadcrumbs) and kept for your plan's retention. The issue, its counts and its daily chart stay.
    • Your data is ours to hold and nobody else's: the events go to serverinspector.com and to no third party.

    Volume, and what happens at the edge

    Free keeps 1,000 events a month, Pro 50,000, Business 500,000. Over that we keep counting the cause and stop storing bodies, and answer with a 429 and a rate-limit header. Within a plan, ten full events an hour per cause are kept plus the newest one: the hundredth copy of the same stack teaches nobody anything.

    Tie it to your deploys

    Send release with the error and record a deploy marker from CI. The issue then reads "first seen in v1.42, deployed 14:32", beside the response-time chart for the same minute.

    npx serverinspector deploy --monitor api --version $GIT_SHA || true
    The CLI and the MCP server

    Already using an error SDK?

    We also accept the wire format the common open-source SDKs send, so an application that already has one keeps its code and changes one variable: set the SDK's DSN to your ServerInspector DSN. Nothing is sent anywhere else; the SDK is a client library, and we are the server it talks to.

    One project, free

    1,000 events a month on the free plan, alongside 10 monitors. No card.

    Create an account