{
  "$defs": {
    "Breadcrumb": {
      "description": "One thing the reporter did before they reported. A short timeline of these turns \"it broke after I clicked save\" into something reproducible.\n\nWhich fields are set depends on the kind: a click or a submit carries the `target` selector (and, for a click, the visible `text`), a navigation carries `from` and `to`, and a visibility change carries `to`.",
      "properties": {
        "from": {
          "description": "Path and query the navigation left, or nothing when it is not known.",
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/BreadcrumbKind"
        },
        "target": {
          "description": "A short CSS selector for the element involved.",
          "type": "string"
        },
        "text": {
          "description": "Visible text of the clicked element, whitespace-collapsed and clipped.",
          "maxLength": 40,
          "type": "string"
        },
        "to": {
          "description": "Path and query navigated to, or `hidden`/`visible` for a visibility change.",
          "type": "string"
        },
        "ts": {
          "description": "ISO 8601 timestamp.",
          "type": "string"
        }
      },
      "required": [
        "ts",
        "kind"
      ],
      "type": "object"
    },
    "BreadcrumbKind": {
      "enum": [
        "click",
        "navigation",
        "submit",
        "visibility"
      ],
      "type": "string"
    },
    "BugReport": {
      "description": "The JSON body a report is sent as. Extra fields may be added by the client.",
      "properties": {
        "breadcrumbs": {
          "description": "What the reporter did before reporting, oldest first.",
          "items": {
            "$ref": "#/$defs/Breadcrumb"
          },
          "maxItems": 30,
          "type": "array"
        },
        "console": {
          "items": {
            "$ref": "#/$defs/ConsoleEntry"
          },
          "maxItems": 50,
          "type": "array"
        },
        "contact": {
          "description": "How to reach the reporter, when the form asked for it and they answered. Free text: an email address, a phone number, a name in your own chat. Off by default everywhere, and personal data the moment it is on — see the privacy section of the README before you store it.",
          "maxLength": 200,
          "type": "string"
        },
        "context": {
          "$ref": "#/$defs/ReportContext"
        },
        "elements": {
          "description": "Elements the reporter pointed at, in the order they were attached.",
          "items": {
            "$ref": "#/$defs/ElementRef"
          },
          "maxItems": 10,
          "type": "array"
        },
        "message": {
          "maxLength": 4000,
          "type": "string"
        },
        "network": {
          "description": "Requests that failed or were slow before the report, oldest first.",
          "items": {
            "$ref": "#/$defs/NetworkEntry"
          },
          "maxItems": 30,
          "type": "array"
        },
        "notes": {
          "description": "What the library had to do to this report on the way out, in its own words: the offline queue dropping a screenshot it could not store is the first and so far only one. It is written by the library, not by the reporter, and it exists so that a reader who sees no picture can tell \"none was taken\" from \"one was taken and would not fit\".",
          "items": {
            "maxLength": 200,
            "type": "string"
          },
          "maxItems": 5,
          "type": "array"
        },
        "perf": {
          "$ref": "#/$defs/PerfSnapshot",
          "description": "What the page cost, when `bugbottle/perf` was measuring."
        },
        "replay": {
          "$ref": "#/$defs/ReplayCapture",
          "description": "The last seconds before the report, when `bugbottle/rrweb` was recording."
        },
        "screenshotDataUrl": {
          "maxLength": 2900000,
          "pattern": "^data:image/png;base64,",
          "type": "string"
        },
        "storage": {
          "$ref": "#/$defs/StorageSnapshot",
          "description": "What was in the browser's stores, when `bugbottle/perf` was measuring."
        },
        "type": {
          "$ref": "#/$defs/ReportType"
        }
      },
      "required": [
        "type",
        "message",
        "context"
      ],
      "type": "object"
    },
    "ConsoleEntry": {
      "properties": {
        "level": {
          "$ref": "#/$defs/ConsoleLevel"
        },
        "message": {
          "maxLength": 500,
          "type": "string"
        },
        "stack": {
          "description": "Frames parsed from an uncaught error or a rejection, innermost first.",
          "items": {
            "$ref": "#/$defs/StackFrame"
          },
          "maxItems": 10,
          "type": "array"
        },
        "ts": {
          "description": "ISO 8601 timestamp.",
          "type": "string"
        }
      },
      "required": [
        "ts",
        "level",
        "message"
      ],
      "type": "object"
    },
    "ConsoleLevel": {
      "enum": [
        "error",
        "warn"
      ],
      "type": "string"
    },
    "ElementRef": {
      "description": "An element the reporter pointed at: what it is, what it says, where it is.",
      "properties": {
        "attributes": {
          "additionalProperties": {
            "maxLength": 200,
            "type": "string"
          },
          "description": "id, name, role, type, href, aria-label, placeholder, title and data-* — never data-bugbottle*.",
          "type": "object"
        },
        "rect": {
          "description": "Page coordinates in CSS pixels.",
          "properties": {
            "height": {
              "type": "number"
            },
            "width": {
              "type": "number"
            },
            "x": {
              "type": "number"
            },
            "y": {
              "type": "number"
            }
          },
          "required": [
            "x",
            "y",
            "width",
            "height"
          ],
          "type": "object"
        },
        "selector": {
          "description": "A short CSS selector, e.g. `form#checkout > button:nth-of-type(2)`.",
          "type": "string"
        },
        "tag": {
          "type": "string"
        },
        "text": {
          "description": "Visible text, whitespace-collapsed and clipped.",
          "maxLength": 200,
          "type": "string"
        }
      },
      "required": [
        "selector",
        "tag",
        "text",
        "rect",
        "attributes"
      ],
      "type": "object"
    },
    "NetworkEntry": {
      "description": "One request the browser made before the report. Recorded by `bugbottle/network`, which keeps the failed and the slow ones.\n\nBodies and headers are never part of this, in either direction: that is where tokens and personal data live. What is left says which call failed and how long it took, which is the part that explains the report.",
      "properties": {
        "error": {
          "description": "True when the request failed before a status — offline, CORS, aborted.",
          "type": "boolean"
        },
        "method": {
          "description": "The HTTP method, upper case.",
          "type": "string"
        },
        "ms": {
          "description": "How long the request took, in milliseconds.",
          "type": "number"
        },
        "status": {
          "description": "The response status, or 0 when the request never got one.",
          "type": "number"
        },
        "ts": {
          "description": "ISO 8601 timestamp of when the request finished.",
          "type": "string"
        },
        "url": {
          "description": "Path and query, with sensitive query values redacted. Cross-origin URLs keep their origin.",
          "type": "string"
        }
      },
      "required": [
        "ts",
        "method",
        "url",
        "status",
        "ms"
      ],
      "type": "object"
    },
    "PerfSnapshot": {
      "description": "What the page cost the reporter, measured by `bugbottle/perf`.\n\nEvery field is optional because every field is a measurement that may not have happened: a browser without `PerformanceObserver`, a page nobody interacted with, a runtime that does not expose the heap. Milliseconds are whole numbers and `cls` is rounded to three decimals — this is evidence for a reader, not a benchmark.",
      "properties": {
        "cls": {
          "description": "Cumulative Layout Shift, excluding shifts that followed a recent input.",
          "type": "number"
        },
        "domContentLoaded": {
          "description": "When `DOMContentLoaded` finished, in milliseconds from navigation start.",
          "type": "number"
        },
        "inp": {
          "description": "Interaction to Next Paint: the worst interaction, in milliseconds.",
          "type": "number"
        },
        "lcp": {
          "description": "Largest Contentful Paint, in milliseconds from navigation start.",
          "type": "number"
        },
        "load": {
          "description": "When the load event finished, in milliseconds from navigation start.",
          "type": "number"
        },
        "longTasks": {
          "description": "Tasks that blocked the main thread for over 50 ms.",
          "properties": {
            "count": {
              "type": "number"
            },
            "totalMs": {
              "type": "number"
            }
          },
          "required": [
            "count",
            "totalMs"
          ],
          "type": "object"
        },
        "memory": {
          "description": "The JS heap, where the browser exposes it. Chromium only.",
          "properties": {
            "limitMB": {
              "type": "number"
            },
            "usedMB": {
              "type": "number"
            }
          },
          "required": [
            "usedMB",
            "limitMB"
          ],
          "type": "object"
        },
        "ttfb": {
          "description": "Time to First Byte, in milliseconds from navigation start.",
          "type": "number"
        }
      },
      "type": "object"
    },
    "ReplayCapture": {
      "description": "A session replay: the events and how long they cover.\n\nThis is the heaviest and the most sensitive thing a report can carry — a recording of somebody using your application — so it is only ever here because the application called `attachRrweb` and handed its own rrweb `record` in.",
      "properties": {
        "events": {
          "description": "rrweb events, oldest first, starting at a full snapshot.",
          "items": {
            "$ref": "#/$defs/ReplayEvent"
          },
          "maxItems": 20000,
          "type": "array"
        },
        "seconds": {
          "description": "How many seconds the events span, rounded.",
          "type": "number"
        }
      },
      "required": [
        "events",
        "seconds"
      ],
      "type": "object"
    },
    "ReplayEvent": {
      "additionalProperties": {},
      "description": "One rrweb event, as rrweb wrote it.\n\nOnly `type` and `timestamp` are read — by the buffer in `bugbottle/rrweb` to find the checkouts, and by the validator to tell an event from whatever else arrived in the array. The rest is rrweb's payload and is carried through unread, because this library does not know how to play a replay and should not pretend to. That is also why the whole thing is bounded by size rather than field by field: it is somebody else's format.",
      "properties": {
        "timestamp": {
          "description": "Epoch milliseconds.",
          "type": "number"
        },
        "type": {
          "description": "rrweb's event type number. 2 is a full snapshot.",
          "type": "number"
        }
      },
      "required": [
        "type",
        "timestamp"
      ],
      "type": "object"
    },
    "ReportContext": {
      "description": "Where the reporter was, and in what.\n\nEverything after `userAgent` is optional and best-effort: a browser that does not offer a fact simply leaves it out. None of it identifies a person more than the user agent already does.",
      "properties": {
        "colorScheme": {
          "description": "What `prefers-color-scheme` said at the time of the report.",
          "enum": [
            "dark",
            "light"
          ],
          "type": "string"
        },
        "connection": {
          "description": "The Network Information API's effective type, e.g. `4g`.",
          "maxLength": 16,
          "type": "string"
        },
        "language": {
          "description": "The browser's preferred language tag, e.g. `en-GB`.",
          "maxLength": 35,
          "type": "string"
        },
        "online": {
          "description": "Whether the browser believed it was online.",
          "type": "boolean"
        },
        "screen": {
          "description": "`${screenWidth}x${screenHeight}@${devicePixelRatio}`.",
          "maxLength": 32,
          "type": "string"
        },
        "timezone": {
          "description": "IANA time zone the browser resolved, e.g. `Europe/Copenhagen`.",
          "maxLength": 64,
          "type": "string"
        },
        "url": {
          "description": "Path and query of the page. The origin and the fragment are left out.",
          "type": "string"
        },
        "userAgent": {
          "type": "string"
        },
        "viewport": {
          "description": "`${innerWidth}x${innerHeight}`.",
          "type": "string"
        }
      },
      "required": [
        "url",
        "viewport",
        "userAgent"
      ],
      "type": "object"
    },
    "ReportType": {
      "enum": [
        "bug",
        "idea",
        "other"
      ],
      "type": "string"
    },
    "StackFrame": {
      "description": "One line of a parsed stack: where the code was, never what it said. Source text is deliberately absent — a frame points at a file and a position, and resolving that to a line of code is the reader's job, with their own maps.",
      "properties": {
        "col": {
          "description": "1-based column number.",
          "type": "number"
        },
        "file": {
          "description": "Script the frame is in: a URL or a path, as the browser wrote it.",
          "maxLength": 200,
          "type": "string"
        },
        "fn": {
          "description": "Function name, when the browser named one.",
          "maxLength": 200,
          "type": "string"
        },
        "line": {
          "description": "1-based line number.",
          "type": "number"
        }
      },
      "required": [
        "file",
        "line",
        "col"
      ],
      "type": "object"
    },
    "StorageKeyRef": {
      "description": "One key of a web storage: its name and how long its value was. Never the value.",
      "properties": {
        "key": {
          "description": "The key, clipped.",
          "maxLength": 100,
          "type": "string"
        },
        "length": {
          "description": "How many characters the value had.",
          "type": "number"
        }
      },
      "required": [
        "key",
        "length"
      ],
      "type": "object"
    },
    "StorageSnapshot": {
      "description": "What was in the browser's stores when the report was written.\n\nNames and lengths, never values — a key called `authToken` says the state the page was in, and its value says rather more than a bug report should. `values` is the one exception and it is opt-in per key: `initPerf` copies a value in only when the integrator named that key in `allowValues`.",
      "properties": {
        "cookies": {
          "description": "Cookie names. Never cookie values, allow-list or not.",
          "items": {
            "maxLength": 100,
            "type": "string"
          },
          "maxItems": 100,
          "type": "array"
        },
        "local": {
          "description": "`localStorage` keys, in the order the browser lists them.",
          "items": {
            "$ref": "#/$defs/StorageKeyRef"
          },
          "maxItems": 50,
          "type": "array"
        },
        "session": {
          "description": "`sessionStorage` keys, in the order the browser lists them.",
          "items": {
            "$ref": "#/$defs/StorageKeyRef"
          },
          "maxItems": 50,
          "type": "array"
        },
        "values": {
          "additionalProperties": {
            "maxLength": 200,
            "type": "string"
          },
          "description": "Values of the allow-listed keys, clipped.",
          "type": "object"
        }
      },
      "type": "object"
    }
  },
  "$id": "https://bugbottle.dev/schema/report.json",
  "$ref": "#/$defs/BugReport",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "The JSON body bugbottle POSTs to your endpoint. Generated from the BugReport type; the maxLength and maxItems ceilings are the ones the server-side validators enforce.",
  "title": "bugbottle report"
}
