From dc34f40d9a9628f87e3ebfe1b19d21af86b23baa Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Wed, 1 Jul 2026 12:15:35 -0400 Subject: [PATCH 1/5] Point updater + all repo links at AssemblyAI/blurt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project now lives at github.com/AssemblyAI/blurt, so: - AutoUpdater fetches releases from owner "AssemblyAI" (was "alexkroman"), so the in-app self-update pulls from this repo's GitHub Releases. - All github.com/alexkroman/blurt links → github.com/AssemblyAI/blurt across README, site, SECURITY, CONTRIBUTING, issue templates, and the launch-metrics skill's default repo. Left CODE_OF_CONDUCT's @alexkroman link alone — that's the maintainer's personal contact for reporting, not a repository link. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/launch-metrics/metrics.sh | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- App/Blurt/Blurt/Update/AutoUpdater.swift | 4 ++-- CONTRIBUTING.md | 2 +- README.md | 6 +++--- SECURITY.md | 4 ++-- site/index.html | 10 +++++----- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.claude/skills/launch-metrics/metrics.sh b/.claude/skills/launch-metrics/metrics.sh index 5993d813..e4ecbcbc 100755 --- a/.claude/skills/launch-metrics/metrics.sh +++ b/.claude/skills/launch-metrics/metrics.sh @@ -7,7 +7,7 @@ # shellcheck disable=SC2016 set -euo pipefail -REPO="${BLURT_REPO:-alexkroman/blurt}" +REPO="${BLURT_REPO:-AssemblyAI/blurt}" if ! command -v gh >/dev/null 2>&1; then echo "gh CLI not found — install it and run 'gh auth login'." >&2 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 992f8293..c15c9519 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - For security issues, don't file a public issue — see [SECURITY.md](https://github.com/alexkroman/blurt/blob/main/SECURITY.md). + For security issues, don't file a public issue — see [SECURITY.md](https://github.com/AssemblyAI/blurt/blob/main/SECURITY.md). - type: textarea id: what-happened attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d579f4e6..1abaabc4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Question or discussion - url: https://github.com/alexkroman/blurt/discussions + url: https://github.com/AssemblyAI/blurt/discussions about: Ask a question or share an idea that isn't a bug or feature request. - name: Security vulnerability - url: https://github.com/alexkroman/blurt/security/advisories/new + url: https://github.com/AssemblyAI/blurt/security/advisories/new about: Report a security issue privately — please don't open a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index a77b89d3..37ecf746 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Blurt aims to stay small and focused — see ["Things deliberately not here"](https://github.com/alexkroman/blurt/blob/main/AGENTS.md) for what's intentionally out of scope. + Blurt aims to stay small and focused — see ["Things deliberately not here"](https://github.com/AssemblyAI/blurt/blob/main/AGENTS.md) for what's intentionally out of scope. - type: textarea id: idea attributes: diff --git a/App/Blurt/Blurt/Update/AutoUpdater.swift b/App/Blurt/Blurt/Update/AutoUpdater.swift index 6c3d9a54..2df3ee67 100644 --- a/App/Blurt/Blurt/Update/AutoUpdater.swift +++ b/App/Blurt/Blurt/Update/AutoUpdater.swift @@ -7,7 +7,7 @@ import OSLog /// AppUpdater ships no UI of its own — it just fetches GitHub Releases, verifies /// the download was signed by the same Developer ID as the running app, and (on /// request) swaps the bundle in place and relaunches. So this type supplies the -/// UI: on launch it checks for a newer release of `alexkroman/blurt` and, when +/// UI: on launch it checks for a newer release of `AssemblyAI/blurt` and, when /// one exists, presents a Sparkle-style modal offering **Install and Relaunch** /// or **Later** (mirroring `SPUStandardUserDriver`'s update-available alert). /// @@ -16,7 +16,7 @@ import OSLog /// which matches `blurt-`. See `scripts/release-publish.sh`. @MainActor final class AutoUpdater { - private let updater = AppUpdater(owner: "alexkroman", repo: "blurt") + private let updater = AppUpdater(owner: "AssemblyAI", repo: "blurt") private let log = Logger(subsystem: "dev.alex.blurt", category: "update") /// Supplies the window to host the update prompt as a sheet (see `runAlert`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6eb2195..bbcc92ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ If you install the git hooks, `check.sh` runs automatically on every commit. ## Reporting bugs and ideas -Open an [issue](https://github.com/alexkroman/blurt/issues) using one of the +Open an [issue](https://github.com/AssemblyAI/blurt/issues) using one of the templates. For bugs, include your macOS version and steps to reproduce. For a security issue, don't open a public issue — see [`SECURITY.md`](./SECURITY.md). diff --git a/README.md b/README.md index c35da6e2..56cea243 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- + Download Blurt.dmg

- + macOS 15 or laterBlurt

@@ -121,7 +121,7 @@

Blurt your first draft.

Download Blurt.dmg @@ -149,7 +149,7 @@

Blurt your first draft.

Powered by
AssemblyAI.

From 40a2764ba27c3fd22d295d98a7fb21070a1fc29e Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Wed, 1 Jul 2026 12:38:33 -0400 Subject: [PATCH 2/5] Replace Sentry with Datadog monitoring (dd-sdk-ios) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the crash/error-reporting stack from sentry-cocoa to the Datadog Swift SDK (dd-sdk-ios 3.13.0). New `Monitoring` module (Release-only, no PII, US1) wires: - DatadogCore + DatadogCrashReporting — crash reports (Sentry crash equiv). - DatadogLogs — handled faults reported as error logs via reportError(), preserving AppCoordinator's fault filtering + underlying_error attribute (Sentry capture(error:) equiv). Both feed Datadog Error Tracking. RUM is intentionally not used: DatadogRUM links UIKit and does not build for native (AppKit) macOS — only Mac Catalyst. Logs + Crash Reporting are the supported best-practice stack for a native macOS app. Also: remove the Sentry SPM dependency + release-build.sh sentry-cli (dSYM/release) steps, replaced with best-effort `datadog-ci dsyms upload` (guarded on DATADOG_API_KEY); update README/SECURITY/release skill wording and the codeql cache comment. Verified: app builds on macos (arch=arm64, warnings-as-errors); shellcheck, swift-format, actionlint, prettier, markdownlint all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/release/SKILL.md | 21 ++--- .github/workflows/codeql.yml | 2 +- App/Blurt/Blurt.xcodeproj/project.pbxproj | 54 +++++++++---- .../xcshareddata/swiftpm/Package.resolved | 37 +++++++-- App/Blurt/Blurt/AppCoordinator.swift | 19 ++--- App/Blurt/Blurt/AppDelegate.swift | 24 ++---- App/Blurt/Blurt/Monitoring/Monitoring.swift | 59 ++++++++++++++ App/Blurt/project.yml | 14 ++-- README.md | 4 +- SECURITY.md | 2 +- .../Pipeline/DictationSession.swift | 2 +- scripts/release-build.sh | 78 +++++++------------ 12 files changed, 200 insertions(+), 116 deletions(-) create mode 100644 App/Blurt/Blurt/Monitoring/Monitoring.swift diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index ca1a1e15..9d4cf4b3 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,6 +1,6 @@ --- name: release -description: Cut and publish a Blurt release (build, sign, notarize, staple, DMG, Sentry, GitHub). Use when the user asks to ship/release a new version. User-invoked only — it has real side effects (notarization, git tags, GitHub release, Sentry release). +description: Cut and publish a Blurt release (build, sign, notarize, staple, DMG, Datadog, GitHub). Use when the user asks to ship/release a new version. User-invoked only — it has real side effects (notarization, git tags, GitHub release, Datadog dSYM upload). disable-model-invocation: true --- @@ -12,9 +12,9 @@ the target version with the user before publishing** — publishing is hard to u ## Preconditions (verify first) - Clean working tree on an up-to-date `main` (releases ship from `main`). -- `sentry-cli` **authenticated** — `sentry-cli login` session or `SENTRY_AUTH_TOKEN` - in the env. `release-build.sh` preflight fails fast otherwise. The Sentry org is - pinned as the **slug** (`alex-kroman`), not the numeric ID, or sentry-cli warns. +- (Optional) `DATADOG_API_KEY` in the env for dSYM upload — needs Node/`npx` + (datadog-ci). `DATADOG_SITE` defaults to `datadoghq.com` (US1). If unset, the + build skips symbol upload with a warning rather than failing. - Notary profile `blurt-notary` exists (`xcrun notarytool store-credentials`). - Developer ID signing identity present in the keychain. @@ -24,11 +24,11 @@ the target version with the user before publishing** — publishing is hard to u and build number in `App/Blurt/project.yml`, regenerates the project, and opens a PR (versions land on `main` via PR — `main` is branch-protected). 2. **Build + sign + notarize** — `scripts/release-build.sh` does the whole Apple - path: `xcodebuild` Release → sign nested code **including embedded - frameworks** (`Sentry.framework` must get `--options runtime --timestamp`, or - notarization rejects it) → notarize → staple → DMG → verify. It also uploads - the dSYM to Sentry and creates/finalizes the matching Sentry release - (`dev.alex.blurt@+`). + path: `xcodebuild` Release → sign nested code **including any embedded + frameworks** (each must get `--options runtime --timestamp`, or notarization + rejects it) → notarize → staple → DMG → verify. If `DATADOG_API_KEY` is set it + also uploads the dSYM to Datadog (via `npx @datadog/datadog-ci`) so crashes + symbolicate. 3. **Publish** — `scripts/release-publish.sh` creates the GitHub release and uploads `Blurt.dmg` (the README's download link points at `releases/latest/download/Blurt.dmg`). @@ -41,7 +41,8 @@ the target version with the user before publishing** — publishing is hard to u - Notarization rejects any nested mach-o/framework lacking a **secure timestamp**; the build re-signs frameworks for this reason — don't remove that. - After a release, verify the DMG mounts, the app is stapled - (`xcrun stapler validate`), and the Sentry release shows the dSYM. + (`xcrun stapler validate`), and — if `DATADOG_API_KEY` was set — Datadog shows + the uploaded dSYM. - The post-build install step copies to `/Applications` (TCC needs a stable path); don't redirect it to DerivedData/`/tmp`. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3ac56585..9b1b898e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -69,7 +69,7 @@ jobs: with: persist-credentials: false - # Cache the resolved SwiftPM checkouts so we don't re-clone sentry-cocoa's + # Cache the resolved SwiftPM checkouts so we don't re-clone the SDKs' # full git history on every run. Keyed on the pinned Package.resolved, so a # dependency bump busts the cache. restore-keys lets an older cache seed a # partial hit when the pins change. diff --git a/App/Blurt/Blurt.xcodeproj/project.pbxproj b/App/Blurt/Blurt.xcodeproj/project.pbxproj index 0bfcc269..a3ab763a 100644 --- a/App/Blurt/Blurt.xcodeproj/project.pbxproj +++ b/App/Blurt/Blurt.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ 5E42262A8153912B13356293 /* juno-96-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 63AA5C73111EA1F4E03A8C24 /* juno-96-start.m4a */; }; 5EF49E6114C9C7BF79F857D9 /* juno-8-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 2FFA326076660C91492A6CE1 /* juno-8-stop.m4a */; }; 60CE1FFB2833C564F335DFF3 /* juno-47-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 11F215137F50A2D4AC61E414 /* juno-47-stop.m4a */; }; + 6179B457E48877787CE028DA /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 45CD7152E18BFEE0F598216D /* DatadogCrashReporting */; }; 623F42C6E50E5B9EBF9F207A /* juno-22-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = AD303A24D4133CE3F1F1DE23 /* juno-22-start.m4a */; }; 643FE1E9922319122B1C087D /* rom1a-17-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = ED3C851EF55F6BBEBFDAE53A /* rom1a-17-stop.m4a */; }; 645A40A3B0E75729A0E3D1AC /* juno-66-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0CDBED88D2D7CE84776B7574 /* juno-66-start.m4a */; }; @@ -205,6 +206,7 @@ 8061E52E8A874E1BA584A2E7 /* juno-48-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = ED5D54496B6973C9F7AE05F3 /* juno-48-stop.m4a */; }; 80D5E7A6D72094E9FC31CF7C /* juno-92-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 3C54A0D394CE71150BB0DBA8 /* juno-92-stop.m4a */; }; 82B97735B1714774843153EF /* juno-90-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 79F8F36D7C5D074BC5135FEA /* juno-90-start.m4a */; }; + 82FCEE1383634F9B88976992 /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 056BF2B5954A44C3AEEC6DD2 /* DatadogLogs */; }; 84958721E6DEB4D06E1A01B6 /* juno-42-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = F77424E89C3E4B5B787BDFA4 /* juno-42-stop.m4a */; }; 8736CF7025B1EE7C295218D7 /* juno-9-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 8887DCE124B065F8313AC59F /* juno-9-start.m4a */; }; 87B7038A5385E149C978908B /* juno-15-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0386BF281886EDBFE9EC8C61 /* juno-15-start.m4a */; }; @@ -229,6 +231,7 @@ 8E70ED1163B872F8D691CDC5 /* juno-118-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = E084A62CF8F167165466AE30 /* juno-118-start.m4a */; }; 8E89114ADA5FCE4AF9DFB3C0 /* juno-116-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 486F5FE65FF8C13A35C28271 /* juno-116-start.m4a */; }; 8FE4B9B139BB9557FBC5589B /* OverlayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27D7F43295DE2A81EFFDDBC /* OverlayWindowController.swift */; }; + 9108B9B80FE4395A4D6D4F2C /* Monitoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48B1FA4FF13F4A4B56BD27D /* Monitoring.swift */; }; 91C1257866F6E2D4EE65BD09 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2312DE4BC0F4F070830AD7 /* WindowAccessor.swift */; }; 91D775ACB2351468D47731FF /* rom1a-12-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = E2BCA8E305B596F960CEE1AE /* rom1a-12-start.m4a */; }; 9266EA4F50B9D46E9181FBAF /* rom1a-5-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 2E64BCDB280FD0A27D965AA9 /* rom1a-5-start.m4a */; }; @@ -354,7 +357,7 @@ D4782F83F43693371724011D /* rom1b-23-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = EAB0871FD8CB94A4DCF837CC /* rom1b-23-start.m4a */; }; D5927A391D26CAB212D6FBB4 /* rom1b-3-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 49CD6A6815EDDB16BBBCCCB2 /* rom1b-3-start.m4a */; }; D5A84975628548A076CEB3D7 /* juno-118-stop.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 92387F60666853C0AA1BF0B5 /* juno-118-stop.m4a */; }; - D6DFF322638007525EBA02B2 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 39EB6BB06E04EA9CD2CDC3BA /* Sentry */; }; + D6DFF322638007525EBA02B2 /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = A0E6571699034CB1C08CB0C9 /* DatadogCore */; }; D750C1BF16502EEC92182ECD /* PermissionsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DE60178ABA2CC7F4EB6401 /* PermissionsStepView.swift */; }; D779D9FD904DA9DDC241DA5F /* juno-70-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = AD5F1BDEB1DB3658DDB96247 /* juno-70-start.m4a */; }; D7CCAEAF551027586E39A032 /* juno-4-start.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 63189CC345106B1D56960B1E /* juno-4-start.m4a */; }; @@ -742,6 +745,7 @@ B401FA111CA49F5A535366AC /* rom1b-15-stop.m4a */ = {isa = PBXFileReference; path = "rom1b-15-stop.m4a"; sourceTree = ""; }; B432346450961555004A2BDE /* juno-99-stop.m4a */ = {isa = PBXFileReference; path = "juno-99-stop.m4a"; sourceTree = ""; }; B460358B22184090D1CCD18E /* juno-68-stop.m4a */ = {isa = PBXFileReference; path = "juno-68-stop.m4a"; sourceTree = ""; }; + B48B1FA4FF13F4A4B56BD27D /* Monitoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Monitoring.swift; sourceTree = ""; }; B5F2D2902DFABF0A7F65E712 /* juno-113-stop.m4a */ = {isa = PBXFileReference; path = "juno-113-stop.m4a"; sourceTree = ""; }; B63CC1EF220EB3463D5A4BDF /* rom1b-12-start.m4a */ = {isa = PBXFileReference; path = "rom1b-12-start.m4a"; sourceTree = ""; }; B67F392ADB8F0C2D4BB6827F /* juno-114-start.m4a */ = {isa = PBXFileReference; path = "juno-114-start.m4a"; sourceTree = ""; }; @@ -865,13 +869,23 @@ files = ( 0A7FB8A90BB7E7A5110A4F4D /* BlurtEngine in Frameworks */, A34A51EDE26BC24EB08A3900 /* AppUpdater in Frameworks */, - D6DFF322638007525EBA02B2 /* Sentry in Frameworks */, + D6DFF322638007525EBA02B2 /* DatadogCore in Frameworks */, + 82FCEE1383634F9B88976992 /* DatadogLogs in Frameworks */, + 6179B457E48877787CE028DA /* DatadogCrashReporting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0093069C3061D45801274652 /* Monitoring */ = { + isa = PBXGroup; + children = ( + B48B1FA4FF13F4A4B56BD27D /* Monitoring.swift */, + ); + path = Monitoring; + sourceTree = ""; + }; 10E2E11380C974390308C1D2 /* Products */ = { isa = PBXGroup; children = ( @@ -896,6 +910,7 @@ 7D6DE85EA50E7CD4CC977211 /* Branding */, DF92BBDB4B9C15FAE100B783 /* Hotkey */, 8C67CBF637D85C0F642E53B5 /* MenuBar */, + 0093069C3061D45801274652 /* Monitoring */, 912505E1D0FBD5D5CA591BA2 /* Overlay */, DB755555AE4E89FE376399BF /* Resources */, C9A477A17B95181CF6FCCB13 /* Shared */, @@ -1448,7 +1463,9 @@ packageProductDependencies = ( 371215E93A9C2CE60C6347BB /* BlurtEngine */, B7FF7A7170C36BAABD1ECC90 /* AppUpdater */, - 39EB6BB06E04EA9CD2CDC3BA /* Sentry */, + A0E6571699034CB1C08CB0C9 /* DatadogCore */, + 056BF2B5954A44C3AEEC6DD2 /* DatadogLogs */, + 45CD7152E18BFEE0F598216D /* DatadogCrashReporting */, ); productName = Blurt; productReference = 5E2309BDEF30ED9EFED5B849 /* Blurt.app */; @@ -1485,7 +1502,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( 1B0B6EAFD1174B4B9140D250 /* XCRemoteSwiftPackageReference "AppUpdater" */, - A70C65ACE504420011952EBA /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + 0F8ABF4083EB1DDD289350B7 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */, BFFF3588F0D8C2D0EEF7BC75 /* XCLocalSwiftPackageReference "../.." */, ); preferredProjectObjectVersion = 77; @@ -1955,6 +1972,7 @@ B5A7CEA6517E5A1E4D77623F /* FloatingPanel.swift in Sources */, F42E6D4E62345772C5A49A08 /* HotkeyStepView.swift in Sources */, AD32046355138298E7FC40D0 /* MenuBarScene.swift in Sources */, + 9108B9B80FE4395A4D6D4F2C /* Monitoring.swift in Sources */, 9437C292A6D7FE533338073D /* OverlayView.swift in Sources */, 8FE4B9B139BB9557FBC5589B /* OverlayWindowController.swift in Sources */, D750C1BF16502EEC92182ECD /* PermissionsStepView.swift in Sources */, @@ -2231,33 +2249,43 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */ - 1B0B6EAFD1174B4B9140D250 /* XCRemoteSwiftPackageReference "AppUpdater" */ = { + 0F8ABF4083EB1DDD289350B7 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/mxcl/AppUpdater"; + repositoryURL = "https://github.com/DataDog/dd-sdk-ios"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.1.1; + minimumVersion = 3.13.0; }; }; - A70C65ACE504420011952EBA /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + 1B0B6EAFD1174B4B9140D250 /* XCRemoteSwiftPackageReference "AppUpdater" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/getsentry/sentry-cocoa"; + repositoryURL = "https://github.com/mxcl/AppUpdater"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 9.19.0; + minimumVersion = 2.1.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 056BF2B5954A44C3AEEC6DD2 /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + package = 0F8ABF4083EB1DDD289350B7 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogLogs; + }; 371215E93A9C2CE60C6347BB /* BlurtEngine */ = { isa = XCSwiftPackageProductDependency; productName = BlurtEngine; }; - 39EB6BB06E04EA9CD2CDC3BA /* Sentry */ = { + 45CD7152E18BFEE0F598216D /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 0F8ABF4083EB1DDD289350B7 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCrashReporting; + }; + A0E6571699034CB1C08CB0C9 /* DatadogCore */ = { isa = XCSwiftPackageProductDependency; - package = A70C65ACE504420011952EBA /* XCRemoteSwiftPackageReference "sentry-cocoa" */; - productName = Sentry; + package = 0F8ABF4083EB1DDD289350B7 /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCore; }; B7FF7A7170C36BAABD1ECC90 /* AppUpdater */ = { isa = XCSwiftPackageProductDependency; diff --git a/App/Blurt/Blurt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/App/Blurt/Blurt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4e2e6b0a..27f545c7 100644 --- a/App/Blurt/Blurt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/App/Blurt/Blurt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9d04cd778850e60aa40cee24d02882a4d5ff2794b24c0d2b11957e38a50b5b3d", + "originHash" : "d32b2af4166f93daf367d58fe94ee784347f77b1316b1ac4ad350fb4a26b3d50", "pins" : [ { "identity" : "appupdater", @@ -11,12 +11,39 @@ } }, { - "identity" : "sentry-cocoa", + "identity" : "dd-sdk-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/getsentry/sentry-cocoa", + "location" : "https://github.com/DataDog/dd-sdk-ios", "state" : { - "revision" : "00a2fec32ea665b81e406419e078bb713238e23f", - "version" : "9.19.0" + "revision" : "8446c244769af48f3fdd7e10bcebb3012371511c", + "version" : "3.13.0" + } + }, + { + "identity" : "kscrash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kstenerud/KSCrash.git", + "state" : { + "revision" : "95a8895d75f3c22aa9ad9f2a15d2fbd97b0a55e2", + "version" : "2.5.1" + } + }, + { + "identity" : "opentelemetry-swift-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/open-telemetry/opentelemetry-swift-core", + "state" : { + "revision" : "d77f1400f4c5738f04a1aec0a1246aec5240e9fa", + "version" : "2.5.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" } }, { diff --git a/App/Blurt/Blurt/AppCoordinator.swift b/App/Blurt/Blurt/AppCoordinator.swift index 8b3515cd..dcb8c9da 100644 --- a/App/Blurt/Blurt/AppCoordinator.swift +++ b/App/Blurt/Blurt/AppCoordinator.swift @@ -2,7 +2,6 @@ import AVFoundation import BlurtEngine import Foundation import Observation -import Sentry @MainActor @Observable @@ -328,18 +327,18 @@ final class AppCoordinator { } wasRecording = isRecording - // A .failed phase is a handled error (it doesn't crash the app), so Sentry's - // automatic crash reporting never sees it — report the genuine faults here. + // A .failed phase is a handled error (it doesn't crash the app), so the + // crash reporter never sees it — report the genuine faults here. if case .failed(let error) = phase { - reportFaultToSentry(error) + reportFault(error) } } - /// Reports a pipeline failure to Sentry, but only the genuine faults. The + /// Reports a pipeline failure to Datadog, but only the genuine faults. The /// permission- and key-missing cases are expected setup states the wizard /// already guides the user through, so reporting them would just be noise. The /// underlying error rides along as context for the wrapped cases. - private func reportFaultToSentry(_ error: BlurtError) { + private func reportFault(_ error: BlurtError) { switch error { case .microphonePermissionDenied, .accessibilityPermissionMissing, .apiKeyMissing: return @@ -348,15 +347,13 @@ final class AppCoordinator { // but it's not a fault to report regardless — there was simply nowhere to type. return case .targetAppLost: - SentrySDK.capture(error: error) + Monitoring.reportError(error) case .sttFailed(let underlying), .audioCaptureFailed(let underlying): // A dropped/absent network connection isn't a Blurt fault — it's the // user being offline mid-dictation. The pipeline still surfaces a .failed - // phase to them; we just don't page Sentry about it. + // phase to them; we just don't report it. if Self.isConnectivityError(underlying) { return } - SentrySDK.capture(error: error) { scope in - scope.setExtra(value: String(describing: underlying), key: "underlying_error") - } + Monitoring.reportError(error, attributes: ["underlying_error": String(describing: underlying)]) } } diff --git a/App/Blurt/Blurt/AppDelegate.swift b/App/Blurt/Blurt/AppDelegate.swift index 7b1e6bc7..0834d73f 100644 --- a/App/Blurt/Blurt/AppDelegate.swift +++ b/App/Blurt/Blurt/AppDelegate.swift @@ -2,7 +2,6 @@ import AppKit import BlurtEngine import Foundation import Observation -import Sentry /// Owns the long-lived models and runs launch-time setup. The setup wizard and /// settings UI are SwiftUI `Window` scenes (see `BlurtApp` / `MainWindowRoot` @@ -96,22 +95,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // Crash/error reporting. Started as early as possible so launch-time failures // are captured. Release-only: local dev runs the Debug configuration, and we - // don't want developer crashes/usage polluting production Sentry data. (The - // `SentrySDK.capture` calls in `AppCoordinator` are safe no-ops when the SDK - // was never started, so they self-disable in Debug too.) Blurt never sends - // dictation text or transcripts to Sentry; `sendDefaultPii` is left off so we - // don't attach the reporter's IP either — crash grouping doesn't need it, and - // a dictation app shouldn't ship identifying data it doesn't use. The - // diagnostics this does send are disclosed in README.md and SECURITY.md. + // don't want developer crashes/usage polluting production data. (The + // `Monitoring.reportError` calls in `AppCoordinator` are safe no-ops when the + // SDK was never started, so they self-disable in Debug too.) Blurt never sends + // dictation text or transcripts to Datadog, and no PII is attached — crash + // grouping doesn't need it, and a dictation app shouldn't ship identifying + // data it doesn't use. The diagnostics this does send (env "production") are + // disclosed in README.md and SECURITY.md. #if !DEBUG - SentrySDK.start { options in - options.dsn = "https://8191e48535bcc3fd861f912ae8735e18@o4509792651902976.ingest.us.sentry.io/4511634026004480" - // Pinned rather than left to the SDK's auto-detection: we only start - // Sentry in Release builds (see the `#if !DEBUG`), so every reporting - // session is production, and release-health charts filter on this. - options.environment = "production" - options.sendDefaultPii = false - } + Monitoring.start() #endif // No permission prompts fire at launch. Accessibility (and Microphone) are diff --git a/App/Blurt/Blurt/Monitoring/Monitoring.swift b/App/Blurt/Blurt/Monitoring/Monitoring.swift new file mode 100644 index 00000000..279c1413 --- /dev/null +++ b/App/Blurt/Blurt/Monitoring/Monitoring.swift @@ -0,0 +1,59 @@ +import DatadogCore +import DatadogCrashReporting +import DatadogLogs +import Foundation + +/// Datadog crash + error monitoring. Replaces the former Sentry integration. +/// +/// Started Release-only from `AppDelegate` (local dev runs the Debug config, and +/// we don't want developer crashes/usage polluting production data). Blurt never +/// sends dictation text or transcripts, and no PII is attached — the diagnostics +/// this ships are disclosed in README.md and SECURITY.md. +/// +/// - `CrashReporting` captures crashes — the Sentry crash-reporting equivalent. +/// - A `Logs` logger records handled faults via ``reportError(_:attributes:)`` — +/// the Sentry `capture(error:)` equivalent. Both crashes and error logs feed +/// Datadog Error Tracking. +/// +/// Datadog RUM is intentionally not used: it links UIKit and does not build for +/// native (AppKit) macOS, only Mac Catalyst. Logs + Crash Reporting are the +/// supported best-practice stack for a native macOS app. +/// +/// `@MainActor` so the one-time `start()` and the `logger` it sets are isolated +/// (both callers — `AppDelegate` launch and `AppCoordinator` — are main-actor). +@MainActor +enum Monitoring { + // Public identifiers — safe to ship in the binary, like the old Sentry DSN. + // The client token is from Datadog (US1, datadoghq.com). + private static let clientToken = "pub13781bb00c1c14ae35f16d2b98550454" + private static let service = "blurt" + private static let environment = "production" + + /// Set by `start()`. `reportError` is a no-op until then, so it self-disables + /// in Debug (where the SDK is never started), just like the old Sentry calls. + private static var logger: (any LoggerProtocol)? + + /// Initializes the SDK and enables crash reporting + logging. Call once, at launch. + static func start() { + Datadog.initialize( + with: Datadog.Configuration( + clientToken: clientToken, + env: environment, + site: .us1, + service: service + ), + trackingConsent: .granted + ) + CrashReporting.enable() + Logs.enable() + logger = Logger.create( + with: Logger.Configuration(name: service, networkInfoEnabled: true) + ) + } + + /// Reports a handled fault (the app didn't crash) as an error log, with optional + /// context attributes. A no-op when the SDK was never started (Debug). + static func reportError(_ error: Error, attributes: [String: any Encodable] = [:]) { + logger?.error("dictation pipeline fault", error: error, attributes: attributes) + } +} diff --git a/App/Blurt/project.yml b/App/Blurt/project.yml index a7e1102e..24361f38 100644 --- a/App/Blurt/project.yml +++ b/App/Blurt/project.yml @@ -24,9 +24,9 @@ packages: AppUpdater: url: https://github.com/mxcl/AppUpdater from: "2.1.1" - Sentry: - url: https://github.com/getsentry/sentry-cocoa - from: "9.19.0" + Datadog: + url: https://github.com/DataDog/dd-sdk-ios + from: "3.13.0" targets: Blurt: type: application @@ -78,8 +78,12 @@ targets: product: BlurtEngine - package: AppUpdater product: AppUpdater - - package: Sentry - product: Sentry + - package: Datadog + product: DatadogCore + - package: Datadog + product: DatadogLogs + - package: Datadog + product: DatadogCrashReporting postBuildScripts: - name: Install to /Applications script: | diff --git a/README.md b/README.md index 56cea243..6cae6029 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ you are dictating, then sent over HTTPS to AssemblyAI for transcription. Blurt stores no audio and no transcripts. Release builds send crash reports and a few handled errors to -[Sentry](https://sentry.io). Those reports are diagnostic only: stack traces and -error details, never your audio, transcripts, or API key. +[Datadog](https://www.datadoghq.com). Those reports are diagnostic only: stack +traces and error details, never your audio, transcripts, or API key. Because transcription is processed by AssemblyAI, their policies apply to that audio: diff --git a/SECURITY.md b/SECURITY.md index 2aacba05..6b1b88f1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -33,7 +33,7 @@ how Blurt handles audio, the API key, the clipboard, or the accessibility permissions it uses are in scope here. Release builds also send crash reports and a few handled errors to -[Sentry](https://sentry.io) for stability monitoring. These are diagnostic only +[Datadog](https://www.datadoghq.com) for stability monitoring. These are diagnostic only (stack traces and error details) — they never include your audio, transcripts, or API key, and Blurt does not attach your IP address. Debug builds send nothing. diff --git a/Sources/BlurtEngine/Pipeline/DictationSession.swift b/Sources/BlurtEngine/Pipeline/DictationSession.swift index d6a9b009..94bf5344 100644 --- a/Sources/BlurtEngine/Pipeline/DictationSession.swift +++ b/Sources/BlurtEngine/Pipeline/DictationSession.swift @@ -308,7 +308,7 @@ public actor DictationSession { } catch BlurtError.noEditableTarget { // Not a failure: transcription worked, there was just nowhere to type. The // injector left the text on the clipboard — show the quiet "copied" notice - // rather than the red error flash (and don't page Sentry). + // rather than the red error flash (and don't report it). setPhase(.noTarget) } catch let err as BlurtError { // Surface the injector's real error (e.g. `.targetAppLost`) rather than diff --git a/scripts/release-build.sh b/scripts/release-build.sh index 728d55d0..44882b6c 100755 --- a/scripts/release-build.sh +++ b/scripts/release-build.sh @@ -2,16 +2,12 @@ # Build, sign, notarize, and staple a release DMG into build/release/. # Reproducible; safe to re-run. The publish step is a separate script. # -# Sentry (required): `sentry-cli` must be installed and authenticated — either a -# `sentry-cli login` session or a SENTRY_AUTH_TOKEN in the environment. The auth -# token is the ONLY secret and is never stored in this repo. The org slug + -# project ID below are NOT secrets (the project ID is the same one in the public -# DSN committed in AppDelegate.swift — a DSN ships inside the app binary by -# design), so they're hard-coded here. The build uploads the dSYM (so Release -# crashes symbolicate) -# and creates + finalizes the matching Sentry release (so release-health adoption -# / crash-free-by-release charts populate). Unauthenticated or no sentry-cli → -# preflight fails before building. +# Datadog (optional): if DATADOG_API_KEY is set, the build uploads the dSYM via +# datadog-ci (run through npx, so Node/npm is needed) so Release crashes +# symbolicate in Datadog. The API key is the only secret and is never stored in +# this repo; DATADOG_SITE defaults to datadoghq.com (US1). With no key the upload +# is skipped with a warning — crashes still report, just unsymbolicated — so a +# release never fails on telemetry. set -euo pipefail @@ -26,15 +22,6 @@ readonly IDENTITY="640A7F5A9754400D4A0491E7A6FB30542D907806" readonly TEAM_ID="Y54ZB9JF63" readonly NOTARY_PROFILE="blurt-notary" -# Sentry org slug + project ID (not secrets — see the header note). Exported so -# sentry-cli picks them up without per-command --org/--project flags. The org -# must be the *slug*, not the numeric ID from the DSN: sentry-cli matches the -# slug embedded in the auth token, and a numeric org ID makes it warn and fall -# back to the token's org. (The org is inferable from the token, but pinning it -# keeps the target explicit.) -export SENTRY_ORG="alex-kroman" -export SENTRY_PROJECT="4511634026004480" - SKIP_CHECKS=0 for arg in "$@"; do case "$arg" in @@ -80,16 +67,15 @@ else fi step "Preflight" -for cmd in xcodegen xcodebuild xcrun hdiutil codesign spctl create-dmg awk shasum sentry-cli; do - command -v "$cmd" >/dev/null 2>&1 || die "missing required tool: $cmd (brew install create-dmg / getsentry/tools/sentry-cli if needed)" +for cmd in xcodegen xcodebuild xcrun hdiutil codesign spctl create-dmg awk shasum; do + command -v "$cmd" >/dev/null 2>&1 || die "missing required tool: $cmd (brew install create-dmg if needed)" done -# Sentry auth must be present — the dSYM upload + release creation later depend -# on it, so fail now rather than after the expensive build/notarization. We don't -# dictate *how* it's supplied (a `sentry-cli login` session or a SENTRY_AUTH_TOKEN -# in the environment both work), only that sentry-cli is authenticated. -if sentry-cli info 2>&1 | grep -q "Method: Unauthorized"; then - die "sentry-cli not authenticated — run 'sentry-cli login' (or export SENTRY_AUTH_TOKEN)" +# Datadog dSYM upload is best-effort: warn now if DATADOG_API_KEY is unset so the +# maintainer knows this build's crashes won't symbolicate, but don't block the +# release on it (unlike the Apple signing/notarization steps, which are required). +if [ -z "${DATADOG_API_KEY:-}" ]; then + info "note: DATADOG_API_KEY unset — dSYM upload will be skipped (crashes unsymbolicated)" fi xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1 \ @@ -167,7 +153,7 @@ while IFS= read -r -d '' f; do codesign --force --sign "$IDENTITY" --options runtime --timestamp "$f" NESTED_COUNT=$((NESTED_COUNT + 1)) done < <(find "$APP_STAGED" -type f \( -name "*.dylib" -o -name "*.so" \) -print0) -# 2. Embedded framework bundles (e.g. Sentry.framework). Their mach-o binary has +# 2. Embedded framework bundles, if any. Their mach-o binary has # no dylib/so suffix, so step 1 misses it — sign the bundle so its signature is # refreshed. `-depth` yields the deepest frameworks first, so a nested framework # is signed before any framework that contains it. @@ -261,29 +247,19 @@ rmdir "$MOUNT_POINT" >/dev/null 2>&1 || true trap - EXIT info "dmg contents verified (Blurt.app $MOUNTED_VERSION, signed + stapled)" -step "Sentry (dSYM + release)" -# Runs after all the Apple steps so a Sentry hiccup never wastes notarization. -# Auth + sentry-cli are validated in preflight and org/project are exported -# above, so the commands here need no credentials or --org/--project flags. -# The Sentry release name must match what the in-app SDK auto-tags events with: -# {CFBundleIdentifier}@{CFBundleShortVersionString}+{CFBundleVersion}. -BUILD_NUM="$(awk '/CFBundleVersion:/ {gsub(/"/,"",$2); print $2; exit}' "$APP_DIR/project.yml")" -[ -n "$BUILD_NUM" ] || die "could not parse CFBundleVersion from $APP_DIR/project.yml" -SENTRY_RELEASE="dev.alex.blurt@${VERSION}+${BUILD_NUM}" -info "sentry release: $SENTRY_RELEASE" - -# Symbolicates crashes from the optimized/stripped Release build. -sentry-cli debug-files upload "$DSYM_DST" - -# Create + finalize so adoption / crash-free-by-release / regression charts -# light up. Commit association is best-effort — it needs either a Sentry repo -# integration (--auto) or local git history (--local). -sentry-cli releases new "$SENTRY_RELEASE" -sentry-cli releases set-commits "$SENTRY_RELEASE" --auto \ - || sentry-cli releases set-commits "$SENTRY_RELEASE" --local \ - || info "commit association skipped (no repo integration / git history)" -sentry-cli releases finalize "$SENTRY_RELEASE" -info "sentry: dSYM uploaded + release $SENTRY_RELEASE finalized" +step "Datadog (dSYM upload)" +# Runs after all the Apple steps so a Datadog hiccup never wastes notarization. +# Uploads the Release build's dSYM so optimized/stripped crashes symbolicate in +# Datadog. Best-effort: skipped (with a note) when DATADOG_API_KEY is unset, so a +# release never fails on telemetry. datadog-ci reads DATADOG_API_KEY and +# DATADOG_SITE from the environment; DATADOG_SITE defaults to datadoghq.com (US1). +if [ -n "${DATADOG_API_KEY:-}" ]; then + DATADOG_SITE="${DATADOG_SITE:-datadoghq.com}" \ + npx --yes @datadog/datadog-ci dsyms upload "$DSYM_DST" + info "datadog: dSYM uploaded from $DSYM_DST" +else + info "datadog: DATADOG_API_KEY unset — skipped dSYM upload" +fi step "Provenance" PROVENANCE="$BUILD_ROOT/build-info.txt" From 9ea6f74d59c97585e43145388efa7170930ba63e Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Wed, 1 Jul 2026 12:55:48 -0400 Subject: [PATCH 3/5] Remove the Sentry MCP server from .mcp.json Completes the Sentry removal: drop the now-defunct Sentry MCP server config, leaving only assemblyai-docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .mcp.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.mcp.json b/.mcp.json index 74142651..c502b12b 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,9 +1,5 @@ { "mcpServers": { - "sentry": { - "type": "http", - "url": "https://mcp.sentry.dev/mcp" - }, "assemblyai-docs": { "type": "http", "url": "https://mcp.assemblyai.com/docs" From bd742489c6eaacdcb7ac94111d45ad5b5ea72c69 Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Wed, 1 Jul 2026 13:33:38 -0400 Subject: [PATCH 4/5] Add anonymous install id + launch event to Datadog monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set a random, locally-generated install UUID as Datadog's @usr.id (no name/email/PII) and emit one "app launched" log per run, so unique-install and version-adoption metrics have baseline volume — not just installs that hit an error. Powers the Unique users / Adoption widgets on the Blurt dashboard. Using UUID + UserDefaults also makes `import Foundation` required again, fixing the swiftlint analyze unused_import failure. Disclosed in README.md and SECURITY.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/Blurt/Blurt/Monitoring/Monitoring.swift | 33 ++++++++++++++++++--- README.md | 4 ++- SECURITY.md | 4 ++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/App/Blurt/Blurt/Monitoring/Monitoring.swift b/App/Blurt/Blurt/Monitoring/Monitoring.swift index 279c1413..bf99a80f 100644 --- a/App/Blurt/Blurt/Monitoring/Monitoring.swift +++ b/App/Blurt/Blurt/Monitoring/Monitoring.swift @@ -7,13 +7,17 @@ import Foundation /// /// Started Release-only from `AppDelegate` (local dev runs the Debug config, and /// we don't want developer crashes/usage polluting production data). Blurt never -/// sends dictation text or transcripts, and no PII is attached — the diagnostics -/// this ships are disclosed in README.md and SECURITY.md. +/// sends dictation text or transcripts. The only identifier attached is a random, +/// locally-generated install id (`@usr.id`) used to count unique installs — it +/// carries no personal information and isn't tied to the user's identity. The +/// diagnostics this ships are disclosed in README.md and SECURITY.md. /// /// - `CrashReporting` captures crashes — the Sentry crash-reporting equivalent. /// - A `Logs` logger records handled faults via ``reportError(_:attributes:)`` — /// the Sentry `capture(error:)` equivalent. Both crashes and error logs feed /// Datadog Error Tracking. +/// - `start()` also emits one "app launched" log per run so adoption / unique- +/// install counts have baseline volume (not just installs that hit an error). /// /// Datadog RUM is intentionally not used: it links UIKit and does not build for /// native (AppKit) macOS, only Mac Catalyst. Logs + Crash Reporting are the @@ -29,11 +33,15 @@ enum Monitoring { private static let service = "blurt" private static let environment = "production" + // UserDefaults key holding the anonymous, per-install id set as `@usr.id`. + private static let installIDKey = "dev.alex.blurt.anonymous-install-id" + /// Set by `start()`. `reportError` is a no-op until then, so it self-disables /// in Debug (where the SDK is never started), just like the old Sentry calls. private static var logger: (any LoggerProtocol)? - /// Initializes the SDK and enables crash reporting + logging. Call once, at launch. + /// Initializes the SDK, enables crash reporting + logging, tags telemetry with + /// the anonymous install id, and logs one launch event. Call once, at launch. static func start() { Datadog.initialize( with: Datadog.Configuration( @@ -44,11 +52,18 @@ enum Monitoring { ), trackingConsent: .granted ) + // Anonymous install id only — no name/email/PII — so unique-install and + // version-adoption counts work without identifying anyone. + Datadog.setUserInfo(id: installID) CrashReporting.enable() Logs.enable() - logger = Logger.create( + let logger = Logger.create( with: Logger.Configuration(name: service, networkInfoEnabled: true) ) + Self.logger = logger + // Baseline telemetry: one launch event per run, so adoption / unique-install + // metrics see every install, not only the ones that hit an error. + logger.info("app launched") } /// Reports a handled fault (the app didn't crash) as an error log, with optional @@ -56,4 +71,14 @@ enum Monitoring { static func reportError(_ error: Error, attributes: [String: any Encodable] = [:]) { logger?.error("dictation pipeline fault", error: error, attributes: attributes) } + + /// A random UUID minted once per install and persisted, used as the anonymous + /// `@usr.id`. Not tied to the user's identity; regenerates on a fresh install. + private static var installID: String { + let defaults = UserDefaults.standard + if let existing = defaults.string(forKey: installIDKey) { return existing } + let generated = UUID().uuidString + defaults.set(generated, forKey: installIDKey) + return generated + } } diff --git a/README.md b/README.md index 6cae6029..7e6d8c87 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ stores no audio and no transcripts. Release builds send crash reports and a few handled errors to [Datadog](https://www.datadoghq.com). Those reports are diagnostic only: stack -traces and error details, never your audio, transcripts, or API key. +traces and error details, never your audio, transcripts, or API key. Blurt also +sends an anonymous, random install identifier and app-launch events so we can +measure unique installs and version adoption — neither is tied to your identity. Because transcription is processed by AssemblyAI, their policies apply to that audio: diff --git a/SECURITY.md b/SECURITY.md index 6b1b88f1..4da60103 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,7 +35,9 @@ permissions it uses are in scope here. Release builds also send crash reports and a few handled errors to [Datadog](https://www.datadoghq.com) for stability monitoring. These are diagnostic only (stack traces and error details) — they never include your audio, transcripts, -or API key, and Blurt does not attach your IP address. Debug builds send +or API key, and Blurt does not attach your IP address. To count unique installs +and version adoption, Blurt also attaches a random, anonymous install identifier +(not tied to your identity) and logs an app-launch event. Debug builds send nothing. ## Supported versions From 617756f674f610aeae7c5cb76cf1ce78bf21b0a5 Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Wed, 1 Jul 2026 13:52:49 -0400 Subject: [PATCH 5/5] Gate Datadog start at runtime so periphery sees it as used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitoring.start() was only called inside `#if !DEBUG` in AppDelegate, so periphery (which scans a Debug build) saw it — and everything it uses (clientToken/service/environment/installIDKey/installID) — as unreachable dead code. Call start() unconditionally and move the release gate to a runtime `isReleaseBuild` guard inside it: same behavior (no-op in Debug, no dev telemetry) but the config stays reachable, so periphery is clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/Blurt/Blurt/AppDelegate.swift | 8 ++++---- App/Blurt/Blurt/Monitoring/Monitoring.swift | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/App/Blurt/Blurt/AppDelegate.swift b/App/Blurt/Blurt/AppDelegate.swift index 0834d73f..fa4827ad 100644 --- a/App/Blurt/Blurt/AppDelegate.swift +++ b/App/Blurt/Blurt/AppDelegate.swift @@ -94,7 +94,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { @MainActor func applicationDidFinishLaunching(_ notification: Notification) { // Crash/error reporting. Started as early as possible so launch-time failures - // are captured. Release-only: local dev runs the Debug configuration, and we + // are captured. `start()` is release-only — it no-ops in Debug via a runtime + // guard (called unconditionally, rather than wrapped in `#if !DEBUG`, so the + // config stays reachable for the dead-code scan). Local dev runs Debug, and we // don't want developer crashes/usage polluting production data. (The // `Monitoring.reportError` calls in `AppCoordinator` are safe no-ops when the // SDK was never started, so they self-disable in Debug too.) Blurt never sends @@ -102,9 +104,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // grouping doesn't need it, and a dictation app shouldn't ship identifying // data it doesn't use. The diagnostics this does send (env "production") are // disclosed in README.md and SECURITY.md. - #if !DEBUG - Monitoring.start() - #endif + Monitoring.start() // No permission prompts fire at launch. Accessibility (and Microphone) are // requested only when the user taps the matching button in the setup diff --git a/App/Blurt/Blurt/Monitoring/Monitoring.swift b/App/Blurt/Blurt/Monitoring/Monitoring.swift index bf99a80f..8b641053 100644 --- a/App/Blurt/Blurt/Monitoring/Monitoring.swift +++ b/App/Blurt/Blurt/Monitoring/Monitoring.swift @@ -5,8 +5,9 @@ import Foundation /// Datadog crash + error monitoring. Replaces the former Sentry integration. /// -/// Started Release-only from `AppDelegate` (local dev runs the Debug config, and -/// we don't want developer crashes/usage polluting production data). Blurt never +/// Started from `AppDelegate` at launch; `start()` no-ops in Debug via the +/// `isReleaseBuild` runtime guard (local dev runs the Debug config, and we don't +/// want developer crashes/usage polluting production data). Blurt never /// sends dictation text or transcripts. The only identifier attached is a random, /// locally-generated install id (`@usr.id`) used to count unique installs — it /// carries no personal information and isn't tied to the user's identity. The @@ -36,6 +37,17 @@ enum Monitoring { // UserDefaults key holding the anonymous, per-install id set as `@usr.id`. private static let installIDKey = "dev.alex.blurt.anonymous-install-id" + // Release-only gate. A runtime flag (not `#if` at the call site) so `start()` + // and its config stay reachable for the dead-code scan while execution is still + // limited to Release builds — dev crashes/usage must never hit production. + private static var isReleaseBuild: Bool { + #if DEBUG + false + #else + true + #endif + } + /// Set by `start()`. `reportError` is a no-op until then, so it self-disables /// in Debug (where the SDK is never started), just like the old Sentry calls. private static var logger: (any LoggerProtocol)? @@ -43,6 +55,7 @@ enum Monitoring { /// Initializes the SDK, enables crash reporting + logging, tags telemetry with /// the anonymous install id, and logs one launch event. Call once, at launch. static func start() { + guard isReleaseBuild else { return } Datadog.initialize( with: Datadog.Configuration( clientToken: clientToken,