Telemetry
What Kustodian shares
Kustodian can share two kinds of data, each only if you say yes: Graph compatibility findings, and error reports when Kustodian fails in a way it shouldn’t. Both are off until you opt in, and neither holds your tenant or your data. Separately, you can choose to send a full report for a single error, after you have seen all of it.
This page covers all three: Graph compatibility findings, error reports and full reports. They go to the same place, described below.
Graph compatibility findings: why it helps
Graph doesn’t support every query option on every collection. Some refuse $top=999, some refuse $count, $orderby or $select, some refuse a filter on a nested or multi-valued column, and some won’t list certain columns at all. Kustodian finds out by probing: it sends the request, reads the refusal and tries again without the option. It remembers what it learnt for 7 days, on your machine.
Probing costs time and requests, and some collections, such as sign-in logs, throttle hard. When findings are shared, the ones that hold across tenants can be built into Kustodian, so every user’s queries on those tables skip the probing from the first run.
Kustodian also shares where it falls short: a refusal from Graph it couldn’t work around, a filter it couldn’t send to Graph and ran locally instead, a permission Graph didn’t accept although the schema lists it, and a limit a query hit. Those show which tables, filters and limits to fix first.
Graph compatibility findings: what is sent
One event each time probing finds something new about a table, or a run finds one of the things below. The event is exactly this:
{
"name": "Microsoft.ApplicationInsights.Event",
"time": "2026-11-02T12:00:00.0000000+00:00",
"iKey": "<Kustodian's Application Insights key>",
"tags": { "ai.application.ver": "0.93.0", "ai.location.ip": "0.0.0.0" },
"data": {
"baseType": "EventData",
"baseData": {
"ver": 2,
"name": "GraphProbeFinding",
"properties": {
"path": "auditLogs/signIns",
"finding": "topLimit",
"detail": "500"
}
}
}
}
| Field | What it holds |
|---|---|
path | The table’s Graph path from Kustodian’s built-in schema, such as users or auditLogs/signIns. Never an object ID. For a limit of the whole run, the paths of the tables it read, separated by commas. |
ai.application.ver | Kustodian’s version, so a finding can be matched to the code that made it. |
ai.location.ip | Always 0.0.0.0, so Application Insights derives no location from your address. |
Besides path, each kind of event holds only these properties:
| Event | Sent when | Properties |
|---|---|---|
GraphProbeFinding | Probing finds something new about a table. | finding: topLimit (the largest page size Graph takes; 0 when it takes no $top), refused (Graph refused a clause) or notListed (Graph won’t serve columns in a list). detail: the page size, the clause ($count, $orderby, $select, or the filtered column, such as proxyAddresses/any), or the column names from the schema. |
GraphRefusal | Graph refused a table with a client error Kustodian couldn’t work around (not a permission, sign-in or throttling answer). | status: the HTTP status, such as 400. code: Graph’s error code, such as Request_UnsupportedQuery. Never Graph’s error message, which can quote values from your query. |
GraphPushdownMiss | A where condition ran on your machine instead of being sent to Graph as $filter. | reason: why, as a short code, such as operator or caseInsensitive. column: the column, only when it is one of the table’s columns in the schema. Never the condition or its value. |
GraphPermissionMismatch | Graph refused a table for permission although your sign-in holds a permission the schema lists for it. | permission: that permission, such as AuditLog.Read.All. |
GraphLimitHit | A run hit one of Kustodian’s limits. | limit: which one, such as objectsPerTable, rowsPerRun or engineTimeoutSeconds. value: its value in your settings. column: the relationship column, for the relationship limits. |
Never sent
- Your tenant, account, user name or client ID
- Query text, filter values or parameters
- Results, rows or any directory data
- Columns refused for permission, or left out for a license your tenant lacks (they describe your account, not Graph)
- Graph’s error messages, which can quote your values
- Machine name, OS user, device or install ID
- Usage or clicks
How it is kept small
- Kustodian builds the event itself; no SDK, so nothing is collected on the side
- A finding already known on your machine isn’t sent again, and a run’s event is sent at most once each time Kustodian starts
- A send that fails is dropped, never retried or queued
- Builds from source and forks send nothing: the destination is only built into official releases
Error reports
When Kustodian fails in a way it shouldn’t, it can send one short report so the bug can be found and fixed. That means a bug in Kustodian, not a mistake in your query or a permission you haven’t been given. These send a report:
- the query engine process crashed or sent an answer Kustodian couldn’t read (not running out of memory, timing out or being cancelled, which are the query’s doing)
- a run failed unexpectedly, or an action in the app threw an error Kustodian doesn’t expect
- an unhandled error in the app’s host process or in its web interface
Expected errors, such as a missing permission, a sign-in or consent prompt, a typo in a query or Graph throttling, send nothing. A report is sent at most once for the same problem and at most 20 times each time Kustodian starts.
What an error report is
One event, built by Kustodian itself, of the type Application Insights uses for exceptions. It has the same ai.application.ver and ai.location.ip tags as the events above. This is what it holds:
{
"name": "Microsoft.ApplicationInsights.Exception",
"data": {
"baseType": "ExceptionData",
"baseData": {
"ver": 2,
"exceptions": [{
"typeName": "System.IO.EndOfStreamException",
"message": "engine.crash",
"parsedStack": [
{ "level": 0, "method": "Kustodian.Graph.Engine.GraphEnginePool.RunAsync", "assembly": "Kustodian.Graph" },
{ "level": 1, "method": "<external>", "assembly": "" }
]
}],
"properties": {
"reportId": "K7Q2M9XD",
"kind": "engine.crash",
"os": "Darwin 25.6.0",
"arch": "Arm64",
"runtime": "10.0.0",
"exitCode": "134",
"source": "graph",
"trail": "run.start(graph) +0s; engine.exit(134) +4s"
}
}
}
}
| Field | What it holds |
|---|---|
typeName | The type of the error, and of each error inside it, such as System.IO.EndOfStreamException. For an error in the web interface, its name, such as TypeError. |
message | The kind of failure, such as engine.crash or bridge.failed. Never the error’s own text. |
parsedStack | For each step of the stack, the method name and the assembly it is in, only for Kustodian, its query engine libraries (KustoLoco and Kusto.Language) and any assembly whose name starts with System or Microsoft (.NET itself and Microsoft’s libraries). Only method names are kept. Any other step is replaced by <external>. The web interface is minified, so there a step is the function’s short name and the bundle’s file name with line and column, such as index-abc123.js:1:48210. No file paths. |
reportId | Eight random characters, new for each error. The app shows it as K7Q2-M9XD, so you can quote it. |
kind | The kind of failure again, as a property. |
os, arch, runtime | The OS name and version, the processor architecture and the .NET version. |
exitCode, peakMemoryMb, workerUptimeS | Only for an engine failure: the exit code of the engine process, its peak memory, and how long it had run. |
source | Where the error happened: graph, hunting or arg for a run, or host or ui. |
trail | The last 20 steps before the error, as codes with the seconds between them, such as run.start(graph) +0s; account.consent(graph) +31s. The codes come from a fixed list: a run started or ended (with its source and outcome), an error code Kustodian explained to you, sign-in, consent, a token fetched (Graph or Azure), the engine process started, killed or exited (with its exit code), a setting changed (its name only), and an error in the web interface. Never a value. It is kept in memory and never written to disk. |
Never sent in an error report
- Error messages, or the text of any exception
- Query text, filter values or parameters
- Results, rows, Graph requests or Graph’s answers
- Object IDs, your tenant, account, user name or client ID
- File paths, machine name or OS user name
- Your IP address:
ai.location.ipis always0.0.0.0
How it is kept small
- Kustodian builds the event itself; no SDK, so nothing is collected on the side
- Only type names, method names and codes pass the filter that builds it, never free text
- A send that fails is dropped, never retried or queued, and never slows what you are doing
- Builds from source and forks send nothing: the destination is only built into official releases
Full reports
Next to every error Kustodian shows, you see a report ID and, in official releases, a Send details link. It opens a window with the whole report as text, so the developer can reproduce the problem. A full report may contain data from your tenant: it can hold your query, values in it, Graph’s error text, user names and IDs. That is why it works differently:
- You see all of it first. The text shown is exactly what is sent.
- Every section has a checkbox, so you can leave it out, and the query is editable, so you can blank out values.
- It is sent only when you click Send. Closing the window sends nothing.
- It does not depend on the error reports setting: each click is your own choice for that one report.
The sections, where the error has them:
| Section | What it holds |
|---|---|
| Query, Parameters | The query text, and the names, types and values of its parameters. |
| Message | The error’s message, as shown to you. |
| Exception | The error’s full text and stack, including file paths and, for an engine crash, what the engine process reported as it failed. |
| Bridge method | Which internal action of the app failed. |
| Graph error | The Graph requests of the failing table and Graph’s error answer, which can quote values from your query. |
| Engine output | The last 50 lines the engine process wrote to its error output, each cut to 500 characters. |
| Stack | For an error in the web interface, its stack. |
| Automatic report, Recent events | The error report above, as text, and its trail. |
It is sent as one event named Kustodian.FullErrorReport, with the same report ID, the error’s kind (when the details are still kept) and one property per section you left ticked. Application Insights allows 8,192 characters per property, so a longer section is cut there and ends in … (cut). Kustodian keeps what a full report needs in memory only: the last 10 unexpected and 20 expected errors, each for 30 minutes. If the details are gone by the time you click, the window says so and offers what the page still has. Nothing is written to disk.
Where it goes and how long it is kept
Everything on this page goes to an Azure Application Insights resource in Azure’s North Europe region (Ireland), owned by the Kustodian maintainer. Microsoft processes it as Azure’s data processor. Application Insights stores the client IP as 0.0.0.0, and Kustodian sends 0.0.0.0 as the location IP too, so no address or location is kept. Events and reports are kept for 90 days, then deleted. They are used only to decide which tables get built-in query settings and to find and fix bugs in Kustodian, and are not sold or shared with anyone.
Opting in and out
The first time you start a version of Kustodian that has these, it asks once about each. If you said yes before Kustodian shared run findings, it asks again and shares nothing until you answer. If you said yes to Graph findings, it still asks about error reports, because that is a different thing. Nothing is shared before you answer, and closing a question without answering shares nothing (it asks again at the next start). To change an answer, open Settings and tick or untick Share Graph compatibility findings or Send error reports. It applies at once. The choices are stored as "telemetry" and "errorReports" in settings.json in Kustodian’s data folder. Full reports have no setting: you choose each one.
Check it yourself
Kustodian is open source. The code that builds and sends the event is AppInsightsTelemetry.cs, and what triggers it is SharedProbeCache.cs and SharedRunFindings.cs. For error reports, ErrorScrubber.cs is the filter that decides what may pass, ErrorTrail.cs keeps the trail and ErrorReporter.cs builds and sends the reports. Questions or concerns: open an issue.