Diagnostics embeds JSON in the HTML report so an agent can skip the rendered page
Allow users to easily share Diagnostics with your support team to improve the flow of fixing bugs.
At a glance
- What is it?
- AvdLee's Diagnostics is a Swift library that builds a support report for iOS, iPadOS and macOS and attaches it to an MFMailComposeViewController, and its newer releases embed a structured JSON payload inside the HTML file so a support agent or an AI tool can parse data instead of reading markup. It requires Swift 6.0 and is MIT licensed.
- Who is it for?
- Diagnostics fits an app team that gets support requests and currently asks users to describe their environment in prose, since the default report already covers app metadata, system metadata and per-session system logs.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 29 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The report is the product, and its default contents are the pitch
The feature list is organised around what ends up in the report, and three default reporters do most of the work.
App metadata, system metadata and system logs divided per session. That third item is the one worth pausing on. Per-session division means a user can hand over a bounded window of recent activity rather than an entire log file, and it is the difference between a report a support team can read and one they cannot.
The rest of the list is extension surface. There is a `DiagnosticsReportFilter` protocol for filtering out sensitive data, a custom `DiagnosticsLogger` for adding your own logs, flexible setup to add your own smart insights and your own custom diagnostics, and native cross-platform support across iOS, iPadOS and macOS.
The smart insights feature is more than decoration. The README gives two examples of the kind of statement it generates: a warning that the user is low on storage, and a confirmation that the user is on the latest app version. The second one is the more valuable of the pair, because it eliminates the most common round trip in support, where the first question is always whether the user has updated.
The distribution is integration-first rather than export-first. The library allows you to attach the Diagnostics Report as an attachment to the `MFMailComposeViewController`, which is the framework mail composer, so the report leaves the device through a channel the user already understands and no server is involved.
Setup timing is the one requirement that changes your results
There is one instruction about timing, and it is stated as a requirement rather than a suggestion: set up the `DiagnosticsLogger` as early as possible to catch all the system logs, with `didFinishWithOptions` given as the example.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try DiagnosticsLogger.setup()
} catch {
print("Failed to setup the Diagnostics Logger")
}
return true
}The `try` and the `catch` are part of the contract. Setup can fail, the example handles it by printing, and the README's note that the default report already contains valuable information means a failed setup degrades the report rather than crashing the app.
The reason timing matters is mechanical rather than advisory. A logger installed at launch captures the whole session including the crashes and failures that happen during startup. A logger installed when the user taps a support button captures only what happened after that point, which is precisely the window where nothing interesting happened, because the user tapped the button because the problem already occurred.
This is also why the per-session log division in the default reporters is worth having. It only works if the logger was installed early enough to have a session to divide.
Everything else in the setup is additive. Installing the logger late degrades the report; installing it early gives you the full picture, and there is no configuration knob that recovers logs from before the logger existed.
JSON inside the HTML, and why that shape was chosen
The agent-friendly reports section explains a design decision that is more interesting than it first appears.
Diagnostics reports remain a single `.html` attachment that users can email and open in a browser. Newer reports also embed a structured JSON payload in a script tag with the id `diagnostics-report-data` and the type `application/json`. The instruction to support agents and AI tools is to parse that JSON first instead of loading the rendered HTML.
What the payload contains is enumerated: report metadata, agent hints, structured chapters, session metadata, and log events with levels like `debug`, `system`, `error` and `crash`. The browser view is generated from that JSON rather than assembled separately, which is what keeps the two representations from drifting.
Chapters can still include `legacyHTML` where custom formatting is needed, so the escape hatch is explicit rather than accidental.
For a support workflow that consumes JSON directly, there is a standalone mode instead of embedding:
let report = await DiagnosticsReporter.create(format: .json)That distinction matters when you decide how the report leaves the device. An embedded JSON payload inside an HTML file is best when the same artefact has to serve a human in a browser and a tool in a pipeline. A standalone JSON file is best when only the tool consumes it, and it avoids shipping a rendered page nobody opens.
There is a third piece worth noting. The README states that older on-device log sessions remain readable after users update to a newer Diagnostics version, and the repository includes a reusable analyzer skill in `diagnostics-report-analyzer-skill` for clients that support agent skills or plugins. A user who hit a bug before updating and then updates still has a report that can be read, which is the case most diagnostics tools lose.
Sending the report, and the macOS path is different
The iOS sending path is a mail composer with the report attached, and the example is written as a button action inside a `Task` on the main actor.
Task { @MainActor in
/// Create the report.
let report = await DiagnosticsReporter.create()
guard MFMailComposeViewController.canSendMail() else {
/// For debugging purposes you can save the report to desktop when testing on the simulator.
report.saveToDesktop()
return
}
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = self
mail.setToRecipients(["[email protected]"])
mail.setSubject("Diagnostics Report")
mail.setMessageBody("An issue in the app is making me crazy, help!", isHTML: false)
/// Add the Diagnostics Report as an attachment.
mail.addDiagnosticReport(report)
present(mail, animated: true)
}Three details in that snippet are decisions rather than boilerplate. `DiagnosticsReporter.create()` is awaited, so report generation is not synchronous and the UI shows something during it. The `canSendMail()` guard falls back to `report.saveToDesktop()`, which the comment frames as a debugging aid for the simulator, but which doubles as the behaviour when mail is not configured on the device. And `addDiagnosticReport` is a library method rather than a file attachment API call, so the library decides the filename and content type.
macOS has no `MFMailComposeViewController`, so the README shows the alternative through `NSSharingService`. The report data is written to a file named `Diagnostics-Report.html` inside `NSTemporaryDirectory()`, a previous file at that path is removed first with `try? FileManager.default.removeItem(at:)`, and the service performs with that URL as the item.
The remove-before-write step is not cosmetic. A stale report left at the same path would be attached on a later run, and the user would send last week's diagnostics. The sharing service pattern is a generic share sheet, so what it opens depends on the user's default mail client, but the file content is the same report.
Filtering is a protocol you implement, not a switch you flip
Sensitive data handling is presented as a feature you opt into, and the README is direct about the framing: it could be that your report is containing sensitive data.
You filter by creating a type that conforms to `DiagnosticsReportFilter`, and the example project contains a working example named `DiagnosticsDictionaryFilter`. The name is informative: the filter operates on dictionary-shaped report data, so you decide per key what survives.
That is the right shape for the problem but it is also the honest limitation. Nothing is filtered by default. If your app writes a user identifier, an email address or a token into any log line, it will be in the report, and the developer who added the log is the one who has to know it. A library cannot infer which of your strings are sensitive.
The same applies to `UserDefaultsReporter`, which is how you add preferences to a report. You construct it with the `UserDefaults` instance you want read and the list of keys you want included:
let userDefaultsReporter = UserDefaultsReporter(
userDefaults: UserDefaults(suiteName: "a.userdefaults.instance"),
keys: ["key_1"]
)
let diagnosticsReport = await DiagnosticsReporter.create(using: [userDefaultsReporter])Naming the keys explicitly is the mechanism that keeps this safe. A reporter that dumped every preference would be simpler and would leak whatever you had stashed there, so the allow-list is the design rather than an inconvenience.
The custom logging path works the same way. `DiagnosticsLogger` is the hook for adding your own logs, and the same early-setup requirement applies to it as to the system log capture.
Practically, the review question before shipping is whether you have read the report yourself end to end. A default report plus three custom log lines is a small surface, and reading your own output once is enough to catch a key you did not mean to include.
Swift 6.0, Carthage, and a repository that serves several toolchains
The badges state the requirements directly: language Swift 6.0, Carthage-compatible, MIT licensed.
Swift 6.0 as a hard requirement is the significant one. The package manifest layout shows why the requirement is expressed rather than assumed, with a `Package.swift` and then separate manifests at `[email protected]`, `[email protected]`, `[email protected]` and `[email protected]`. Those versioned manifests are Swift Package Manager's mechanism for serving different toolsets to different consumers, so the package supports older Swift toolchains while still declaring Swift 6.0 as its current language mode.
The repository layout is broader than a single library, and that breadth is the adoption consideration. Alongside `Diagnostics/` and `DiagnosticsTests/` there is `Example/Diagnostics-Example.xcodeproj/` for the sample application, a `fastlane/` directory for release automation, a `Gemfile` and `Gemfile.lock`, and `Package.resolved` for pinned dependencies.
Then there is the agent tooling, which is the unusual part. `diagnostics-report-analyzer-skill/` is a skill directory, and there are parallel `agents/`, `.claude-plugin/` and `.cursor-plugin/` directories plus `plugins.json` and `openai.yml`, which suggests the skill is packaged for several agent hosts rather than one. `package.json` exists solely to declare the pi package format, with a `pi` key pointing its `skills` array at the analyzer skill directory. There is also a `.cursorignore` and an `agent-skill-install-instructions.md` covering Claude, Cursor, OpenAI-compatible tools, pi and manual installation.
Installation of that skill is a single command:
npx skills add https://github.com/AvdLee/Diagnostics --skill diagnostics-report-analyzer-skillMaintenance is current. The repository is not archived, the last push was on 2026-09-26, and the release notes are unusually descriptive for a patch: 7.0.3 is a log trimming performance fix, 7.0.2 is a standard output recursion fix and 7.0.1 is a trimming logs improvement. All three concern log trimming, which suggests the store for log data is where the recent work has been.
Editorial conclusion
Diagnostics fits an app team that gets support requests and currently asks users to describe their environment in prose, since the default report already covers app metadata, system metadata and per-session system logs. Do not adopt it expecting it to solve redaction for you, because sensitive data filtering is an opt-in filter you write rather than a default, and do not assume the JSON payload exists in older reports, since it was added in a later line and on-device log sessions from previous versions remain readable in the legacy format. Verify first where you call `DiagnosticsLogger.setup()`, because the library states it must be as early as possible or earlier logs are simply missing.
Frequently asked questions
What does the AvdLee Diagnostics library do?
It is a Swift library that builds a Diagnostics Report for your app and lets you attach it to an MFMailComposeViewController so a user can email it to your support team. Default reporters cover app metadata, system metadata and system logs divided per session.
What are the requirements for AvdLee Diagnostics?
Swift 6.0 as the language mode, Carthage compatibility, and iOS, iPadOS or macOS. The repository carries versioned Package manifests for Swift 5.5 through 5.8 so older toolchains are served separately.
When should I call DiagnosticsLogger.setup()?
As early as possible, with the README giving `didFinishLaunchingWithOptions` as the example. Setup can throw, and if it is installed late the report simply lacks the earlier session logs, which is usually the part you needed.
How do agents read a Diagnostics report?
Parse the JSON embedded in a script tag with id `diagnostics-report-data` and type `application/json`, rather than reading the rendered HTML. If your workflow only consumes JSON, create a standalone report with `DiagnosticsReporter.create(format: .json)`.
How do I filter sensitive data out of a Diagnostics report?
By creating a type that conforms to `DiagnosticsReportFilter`; the example project includes `DiagnosticsDictionaryFilter`. Filtering is opt-in and nothing is removed by default, so you decide per key what survives.
What license is AvdLee Diagnostics released under?
MIT, with the LICENSE file at the repository root. Recent releases are 7.0.3, a log trimming performance fix, and 7.0.2, a standard output recursion fix.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/avdlee-diagnostics)Was this useful?