DOCUMENTATION

Send your first error

Five minutes, one HTTP call — in curl, PHP, WordPress, Node, the browser, Python, Go or OpenTelemetry.

Five minutes, one HTTP call. No SDK is required and nothing has to be installed on the console side.

1. Get a key

  • In the console, open PROJECTS and create a project.
  • Copy its api_key — 64 hex characters, secret, server-side only.
  • A browser app uses the project's js_key instead. It is public by design and is checked against the project's allowed origins.

Why two keys? A browser cannot keep a secret. The public key identifies the project, and the Origin allowlist does the authenticating — so a leaked js_key buys an attacker nothing but reports from your own domains.

2. Send one error

Every client does the same thing underneath: one POST, a key in a header, JSON in the body. Pick your runtime.

curl
curl -X POST https://yourconsole.cloud/api/v1/ingest \
  -H "X-Console-Key: $CONSOLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "v": 1,
    "runtime": "php",
    "entry": "web",
    "kind": "error",
    "priority": 3,
    "message": "Hello from curl",
    "events": [{"message": "Hello from curl", "className": "RuntimeException"}]
  }'
PHP
$payload = [
	'v' => 1,
	'runtime' => 'php',
	'entry' => 'web',
	'kind' => 'error',
	'priority' => 3,
	'message' => $throwable->getMessage(),
	'events' => [[
		'message' => $throwable->getMessage(),
		'className' => $throwable::class,
		'file' => $throwable->getFile(),
		'line' => $throwable->getLine(),
		'backtrace' => $throwable->getTraceAsString(),
	]],
];

// give the visitor their page BEFORE spending a millisecond on telemetry:
// under php-fpm this flushes the response and ends the request for the
// browser, while this process keeps running
if(function_exists('fastcgi_finish_request'))
{
	fastcgi_finish_request();
}

$curl = curl_init('https://yourconsole.cloud/api/v1/ingest');
curl_setopt_array($curl, [
	CURLOPT_POST => true,
	CURLOPT_POSTFIELDS => json_encode($payload),
	CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Console-Key: ' . $key],
	CURLOPT_TIMEOUT_MS => 1000,        // never let reporting slow a request
	CURLOPT_RETURNTRANSFER => true,
]);

@curl_exec($curl);                     // fire and forget: failures are swallowed
WordPress
Install the plugin, paste the console URL and the api_key in
Settings → ovos console, and press "Send test error".

	https://github.com/ovos/console-client-wordpress

PHP errors, JavaScript errors, failed logins and scanner probes are
reported from then on, with no code in your theme.
Node.js
const report = async (error) => {
	const body = JSON.stringify({
		v: 1,
		runtime: 'node',
		entry: 'web',
		kind: 'error',
		priority: 3,
		message: error.message,
		events: [{message: error.message, className: error.name, backtrace: error.stack}],
	});

	// never await this on the request path, and never retry
	await fetch('https://yourconsole.cloud/api/v1/ingest', {
		method: 'POST',
		headers: {'Content-Type': 'application/json', 'X-Console-Key': process.env.CONSOLE_KEY},
		body,
		signal: AbortSignal.timeout(1000),
	}).catch(() => {});
};
Browser
<script src="https://yourconsole.cloud/js/console-client.js"></script>
<script>
	ovosConsole.init({
		url: 'https://yourconsole.cloud',
		key: 'YOUR_PUBLIC_JS_KEY',
	});
</script>
Python
import json, urllib.request

def report(exc: BaseException, key: str) -> None:
	body = json.dumps({
		"v": 1,
		"runtime": "python",
		"entry": "web",
		"kind": "error",
		"priority": 3,
		"message": str(exc),
		"events": [{"message": str(exc), "className": type(exc).__name__}],
	}).encode()

	req = urllib.request.Request(
		"https://yourconsole.cloud/api/v1/ingest",
		data=body,
		headers={"Content-Type": "application/json", "X-Console-Key": key},
	)

	try:
		urllib.request.urlopen(req, timeout=1)
	except Exception:
		pass                      # reporting may never break the caller
Go
body, _ := json.Marshal(map[string]any{
	"v":        1,
	"runtime":  "go",
	"entry":    "web",
	"kind":     "error",
	"priority": 3,
	"message":  err.Error(),
	"events":   []map[string]any{{"message": err.Error(), "className": "error"}},
})

req, _ := http.NewRequest("POST", "https://yourconsole.cloud/api/v1/ingest", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Console-Key", key)

client := &http.Client{Timeout: time.Second}
resp, err := client.Do(req)          // ignore the error: never fail the caller
if err == nil {
	resp.Body.Close()
}
Java
var payload = """
	{"v":1,"runtime":"java","entry":"web","kind":"error","priority":3,
	 "message":"%s","events":[{"message":"%s","className":"%s"}]}
	""".formatted(e.getMessage(), e.getMessage(), e.getClass().getName());

var request = HttpRequest.newBuilder(URI.create(console + "/api/v1/ingest"))
	.header("Content-Type", "application/json")
	.header("X-Console-Key", key)
	.timeout(Duration.ofSeconds(1))          // hard cap, always
	.POST(HttpRequest.BodyPublishers.ofString(payload))
	.build();

// fire and forget: the future is never joined on the request path
HttpClient.newHttpClient()
	.sendAsync(request, HttpResponse.BodyHandlers.discarding())
	.exceptionally(t -> null);
Rust
// reqwest, but any client works — this is one POST
let payload = serde_json::json!({
    "v": 1,
    "runtime": "rust",
    "entry": "web",
    "kind": "error",
    "priority": 3,
    "message": err.to_string(),
    "events": [{ "message": err.to_string(), "className": "Error" }],
});

let client = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(1))
    .build()?;

// spawned, never awaited by the handler: reporting must not add latency
tokio::spawn(async move {
    let _ = client
        .post(format!("{console}/api/v1/ingest"))
        .header("X-Console-Key", key)
        .json(&payload)
        .send()
        .await;                       // errors are deliberately ignored
});
OpenTelemetry
# already instrumented? point the collector's logs pipeline at the console
exporters:
  otlphttp/console:
    logs_endpoint: https://yourconsole.cloud/api/v1/ingest/otel
    encoding: json
    headers:
      X-Console-Key: ${env:CONSOLE_KEY}

service:
  pipelines:
    logs:
      exporters: [otlphttp/console]

A 202 means it is queued. It appears in the grid within about a second.

3. What you get without doing anything else

  • Grouping. The same fault a thousand times is one issue with a count.
  • Priority. 0–7 on arrival, per project, so the gate is yours.
  • Trace correlation. Send a traceId and a browser failure lines up with the server error behind it.
  • Lifecycle. Open → resolved → regressed, per issue, with history.

4. Then turn on the parts that pay

switchwhat it buyscost
404 reportingscanner probes, counted apart from bugsone flag
security eventsfailed logins, refused actions, privileged changesone flag
traffic rollupserror counts read as rates, not raw numbersAPCu
software inventoryCVE matches against what you actually runone flag

Rules that keep reporting safe

  • Fire and forget. Never await the report on the request path.
  • One second, hard. A console that is down must cost your users nothing.
  • Never retry. A retry storm during an incident is your outage, not ours.
  • Redact before sending. Passwords, tokens and cookies never leave the box.
  • Batch at the end. Collect during the request, send once after the response has gone out.

The long version, with the field reference and the failure rules, is in Build your own client and Ingest API v1.


Next: Build your own client for the rules in full, Browser client for JavaScript errors, or Ingest API v1 for every field.

No client for your stack?

A client is one POST, so anything that speaks HTTP can report. But you do not have to write it:

  • We build CMS clients on request — TYPO3, Drupal, Shopware, or something in-house. Tell us what you run and we write the client and integrate it.
  • WordPress is already done and public, and it is the reference every other client follows.
  • Write to office@ovos.at with the platform and its version.