inlinedocs
Portal ↗

Security & monitoring

Observability protocol

This is the wire format the observability module ingests. It is deliberately small and fully documented so that a language without a first-party SDK is a copy-paste away rather than a support ticket.

The canonical definition lives in packages/core/src/observe/envelope.ts. This page describes exactly that schema.

# Endpoint

text
POST /api/ingest/v1/<publicKey>
Content-Type: application/json

publicKey is the ink_pub_… credential from your project’s DSN. It is public by design.

  • CORS is open (Access-Control-Allow-Origin: *); OPTIONS is handled.
  • Send no credentials — the endpoint ignores cookies, and sending them breaks the wildcard CORS response.
  • Success is 202 Accepted with { "accepted": n, "dropped": n }.

# Response codes

Code Meaning Should a client retry?
202 Accepted (dropped > 0 explains what was filtered and why) No
400 Malformed JSON or schema violation — the body names the field No, it will never succeed
404 Unknown project key No
413 Body over 1 MB No, send smaller batches
429 Rate limited Yes, with backoff
5xx Server-side failure Yes, with backoff

# The envelope

json
{
  "sdk": { "name": "inline-observe.javascript", "version": "0.1.0" },
  "sentAt": "2026-08-15T10:32:00.000Z",
  "environment": "production",
  "release": "1.4.0",
  "releaseCommit": "9f2c1ab…",
  "platform": "javascript",
  "events": [ /* 1–100 events */ ]
}

Only events is required. environment, release, releaseCommit and platform at the envelope level are defaults applied to any event that omits them — so a batching client sends them once.

Three shorthand forms are accepted, for the smallest possible integration:

jsonc
{ "events": [ { … } ] }   // canonical
[ { … }, { … } ]          // a bare array of events
{ "message": "boom" }     // a single bare event

# snake_case is accepted

Every key is normalized, so event_id, release_commit, in_app and context_line parse identically to their camelCase spellings. line_no and line normalize to lineno; col_no and column to colno; abs_path to filename. Write idiomatic Python or Go without a translation layer.

The keys inside tags, extra, contexts, data and headers are your data, not protocol, and are preserved verbatim.

# An event

Field Type Notes
eventId string Your id, for support lookups. Optional.
timestamp ISO 8601 Defaults to receipt time. Rejected if more than 7 days from now — a device with a wrong clock would otherwise pin the issue to 2044 and float it to the top of every list.
type error | message Inferred: error when exception is present, else message.
level fatal | error | warning | info | debug Defaults to error for exceptions, info for messages. Below the project’s floor ⇒ dropped.
platform javascript, node, python, dotnet, go, ruby, java, php, rust, other Display only.
message string Required if there’s no exception.
transaction string Route, job or component name — e.g. POST /checkout. Used as the culprit when no frame is in-app, and as the grouping key for stackless events.
environment string e.g. production, staging.
release string Must match what you upload source maps under.
releaseCommit string The commit this build came from. This is what points the root-cause agent at the right code.
serverName string Instance/host identifier.
exception object See below.
breadcrumbs array Up to 200, oldest → newest.
tags {string: string} Filterable key/values.
contexts object Free-form: browser, os, runtime, device.
extra object Anything else.
user object id, email, username, ipAddress. Hashed server-side into a counting key.
request object url, method, query, headers.
fingerprint string[] Grouping override. Wins over everything.

# exception

json
{
  "exception": {
    "values": [{
      "type": "TypeError",
      "value": "Cannot read properties of undefined (reading 'id')",
      "mechanism": { "type": "onunhandledrejection", "handled": false },
      "stacktrace": { "frames": [ /* … */ ] }
    }]
  }
}

values is an array so chained/caused-by exceptions have somewhere to live; the first entry is the one that groups and titles the issue.

# Frames — newest first

This is the one place inline differs from what you may expect. Sentry orders frames oldest-first, with the crash at the bottom. inline orders them newest first: frames[0] is where the error was thrown. Every reader — human or model — wants the crash site first, and it’s also the order every JS engine already produces.

Field Type Notes
filename string Absolute path or URL.
function string Function or method name.
module string Logical module, if your runtime has one.
lineno, colno number 1-indexed line; column is needed for accurate source-map lookup on minified bundles.
inApp boolean Set this. It decides grouping and what the AI reads first. true = your code, false = dependency/runtime. Inferred if omitted (paths containing node_modules, .next, node:internal etc. are treated as vendor).
contextLine string The source line, if your runtime can read it.
preContext, postContext string[] Up to 10 lines either side.

Server runtimes can usually fill in the context lines directly; for minified browser code inline recovers them from your uploaded source maps.

json
{
  "timestamp": "2026-08-15T10:31:58.220Z",
  "type": "http",
  "category": "fetch",
  "level": "warning",
  "message": "POST /api/checkout → 500",
  "data": { "status": 500, "durationMs": 812 }
}

Ordered oldest → newest, ending at the error. In practice this is the fastest route to a reproduction — the culprit is more often the failed request two rows up than the frame that happened to throw.

# Limits

Limit Value
Request body 1 MB
Events per envelope 100
Frames per exception 100 (deeper tails truncated)
Breadcrumbs per event 100 (newest kept)
Rate limit 120 requests/minute per project per IP
Field lengths Bounded per field by the schema; overlong values are rejected, not silently cut

# Source-map upload

text
POST /api/observe/artifacts?release=<version>&name=<url-path>&commitSha=<sha>&environment=<env>
Authorization: Bearer <write key>
Content-Type: application/json

<the raw .map file as the body>

Metadata in the query string, raw bytes as the body — so curl --data-binary @file works and no multipart parsing is needed anywhere.

name is the URL path of the bundle (not the map), by convention prefixed with ~: ~/_next/static/chunks/page-ab12.js. inline reduces it to a match key the same way it reduces incoming stack-frame URLs, which is how the two meet.

# Worked example

json
{
  "sdk": { "name": "python-manual", "version": "1.0.0" },
  "environment": "production",
  "release": "2026.08.15",
  "release_commit": "9f2c1ab7d4e",
  "events": [{
    "level": "error",
    "platform": "python",
    "transaction": "POST /api/claims",
    "exception": {
      "values": [{
        "type": "KeyError",
        "value": "'member_id'",
        "stacktrace": {
          "frames": [
            { "filename": "app/claims.py", "function": "submit", "line_no": 88, "in_app": true },
            { "filename": "app/routes.py", "function": "handle", "line_no": 42, "in_app": true },
            { "filename": "site-packages/flask/app.py", "function": "dispatch", "line_no": 1502, "in_app": false }
          ]
        }
      }]
    },
    "breadcrumbs": [
      { "category": "db", "message": "SELECT member WHERE id=… → 0 rows", "level": "warning" }
    ],
    "tags": { "tenant": "acme" },
    "user": { "id": "u_8123" }
  }]
}

Reported this way, the issue groups on KeyError + submit/handle, shows the breadcrumb that explains it, counts one affected user — and if the project is linked to a repo, the root-cause agent opens app/claims.py at commit 9f2c1ab7d4e and reads line 88.