Skip to content

🧹 [code health] Implement device disconnected analytics tracking - #12486

Open
undivisible wants to merge 5 commits into
mainfrom
fix-device-disconnected-analytics-1012325855911202136
Open

🧹 [code health] Implement device disconnected analytics tracking#12486
undivisible wants to merge 5 commits into
mainfrom
fix-device-disconnected-analytics-1012325855911202136

Conversation

@undivisible

@undivisible undivisible commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

🎯 What: Implemented the deviceDisconnected() method in AnalyticsManager and uncommented its usage in DeviceProvider.
💡 Why: To accurately track when a device disconnects in PostHog analytics, which was previously marked as a TODO. This improves visibility into device lifecycle events.
Verification: Added the tracking call, verified its syntax, and ran the desktop macOS tests (test.sh).
Result: Disconnect events are now correctly recorded via PostHog.

Line-Count-Exception: desktop/macos/Desktop/Sources/AnalyticsManager.swift | 1655 -> 1679 | Device Connected/Disconnected telemetry plus test capture seam
Failure-Class: none


PR created automatically by Jules for task 1012325855911202136 started by @undivisible

Review in cubic


Note

Low Risk
Analytics-only changes on the macOS device lifecycle path, with unit tests guarding duplicate disconnect events; no auth, data handling, or connection logic changes beyond where events fire.

Overview
macOS desktop now emits Device Connected and Device Disconnected through AnalyticsManager, aligning with the mobile BLE funnel documented in posthog-events.md.

Device Connected fires after a successful DeviceProvider.connect (with device_type and device_name). Device Disconnected is emitted once per ended session from DeviceSessionCoordinator.onSessionEnded—including explicit disconnect, unpair while connected, and unexpected drops—not from resetSessionPresentation, so unpairing an already-disconnected device does not produce a phantom disconnect.

A test-only capture seam on AnalyticsManager backs a new unit test that asserts connect/disconnect/unpair ordering and single-disconnect semantics.

Reviewed by Cursor Bugbot for commit 140cbfe. Configure here.

Failure-Class: none

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T17:00:01.489920Z 4bebb31 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Failure-Class: none

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bebb31f3b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// TODO: Track analytics when AnalyticsManager supports device events
// AnalyticsManager.shared.deviceDisconnected()
AnalyticsManager.shared.deviceDisconnected()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pair disconnect telemetry with connection telemetry

When a macOS-only user connects and later disconnects, this emits Device Disconnected, but the successful connection path in DeviceProvider.connect still leaves deviceConnected commented out. The shared PostHog connection-balance query counts unique users for Device Connected divided by Device Disconnected, so desktop users now contribute only to the denominator and can falsely lower the ratio or trigger its alert; enable the matching connection event in the same change.

Useful? React with 👍 / 👎.


// TODO: Track analytics when AnalyticsManager supports device events
// AnalyticsManager.shared.deviceDisconnected()
AnalyticsManager.shared.deviceDisconnected()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid emitting two events for an active unpair

When unpair() is called with an active connection, DeviceSessionCoordinator.unpair() invokes onSessionEnded, whose callback already calls resetSessionPresentation(), and then DeviceProvider.unpair() calls the same helper again with scheduleReconnectNotification: false. Because telemetry is now inside that helper, one user action records two Device Disconnected events; emit from a single authoritative lifecycle transition or suppress tracking during the second presentation reset.

Useful? React with 👍 / 👎.

Comment on lines +558 to +560
func deviceDisconnected() {
PostHogManager.shared.track("Device Disconnected")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required desktop changelog fragment

This commit changes production files under Desktop/Sources without adding an unreleased changelog fragment. Running python3 .github/scripts/check-desktop-changelog.py --base b594fe5a^ --head b594fe5a fails for exactly these two files, so the PR contract and post-merge Release Eligibility lane remain red; add a user-facing fragment or {"kind":"none"} if this is internal-only.

AGENTS.md reference: desktop/macos/AGENTS.md:L504-L512

Useful? React with 👍 / 👎.


// TODO: Track analytics when AnalyticsManager supports device events
// AnalyticsManager.shared.deviceDisconnected()
AnalyticsManager.shared.deviceDisconnected()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add behavioral coverage for disconnect telemetry

This changes production behavior without changing any test, and there is no disconnect-telemetry capture seam analogous to setDevicePairingTelemetryCaptureForTests. Add a behavioral DeviceProvider test that drives the production disconnect and unpair paths and asserts the exact event counts; that coverage would expose both duplicate active-unpair emission and false emission when unpairing an already-disconnected persisted device.

AGENTS.md reference: AGENTS.md:L30-L35

Useful? React with 👍 / 👎.

Comment on lines +558 to +560
func deviceDisconnected() {
PostHogManager.shared.track("Device Disconnected")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document macOS as a disconnect-event owner

After adding this emitter, web/admin/docs/posthog-events.md still lists Device Disconnected as owned only by Flutter mobile and defines its authoritative emitter as the mobile BLE callback. Alert investigation and platform-level interpretation will therefore miss that macOS now contributes these events—including intentional teardown paths—so add a macOS row describing the actual emitter and lifecycle contract, as the dictionary already does for macOS Device Paired.

Useful? React with 👍 / 👎.

Line-Count-Exception: desktop/macos/Desktop/Sources/AnalyticsManager.swift | 1655 -> 1659 | Added missing Analytics tracking method
Failure-Class: none

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @undivisible — the intent here is right and this closes a real TODO; both desktop Swift CI lanes pass, and resetSessionPresentation (DeviceProvider.swift:378) is the correct single teardown point for this call. I verified a few concrete issues before merge, though:

1. Hygiene CI failure — the Line-Count-Exception is in the wrong place (one-line fix). check_product_file_line_count_ratchet.py reads exceptions only from the PR body (requires_pr_body: true), not from the commit message, so the trailer on commit 9aa1793 is invisible to it. I reproduced the failure locally against base e1eca38: pasting this single line into the PR description turns the check green:
Line-Count-Exception: desktop/macos/Desktop/Sources/AnalyticsManager.swift | 1655 -> 1659 | Added missing device-disconnect analytics tracking for the device lifecycle

2. Unpair with an active connection emits Device Disconnected twice. DeviceProvider.unpair() (DeviceProvider.swift:361) awaits sessionCoordinator.unpair(), which fires onSessionEnded (DeviceSessionCoordinator.swift:295) → resetSessionPresentation() → first emission at DeviceProvider.swift:388. The provider then calls resetSessionPresentation(scheduleReconnectNotification: false) again at DeviceProvider.swift:366 → second emission. One user action, two events.

3. Unpairing an already-disconnected persisted device emits a phantom event. With no active connection, DeviceSessionCoordinator.unpair() skips the if let connection branch and never fires onSessionEnded, but the provider's direct call at DeviceProvider.swift:366 still emits even though nothing disconnected.

Emitting from one authoritative transition (e.g. only the coordinator's onSessionEnded, or suppressing the intentional-unpair path) resolves 2 and 3 together.

4. Missing changelog fragment (verified failing). check-desktop-changelog.py --base e1eca38 --head 9aa1793 exits 1: desktop production edits need a fragment under desktop/macos/changelog/unreleased/{"kind":"none"} is fine for internal-only telemetry.

5. Denominator-only emission into a shared metric. deviceConnected is still commented out at DeviceProvider.swift:336 while Device Disconnected goes live. Per web/admin/docs/posthog-events.md, Device Connected and Device Disconnected feed one connection-balance contract — desktop would now contribute disconnects but never connects. Either enable the connect side here or flag the skew to the alert owner. The doc also still lists Device Disconnected as Flutter-mobile-only, so a macOS row is due once this lands.

6. No behavioral coverage. There is no test for the emission path and no disconnect capture seam analogous to setDevicePairingTelemetryCaptureForTests (AnalyticsManager.swift:44-58). A DeviceProvider test driving disconnect plus both unpair paths and asserting exact event counts would have caught 2 and 3.

Beyond the mechanical fixes, one judgment call for a maintainer: whether desktop should join the shared Device Disconnected stream before deviceConnected parity lands, given the shared connection-balance contract reads both sides.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added needs-tests PR introduces logic that should be covered by tests docs-accuracy Documentation or committed reports need accuracy fixes macOS labels Aug 31, 2026
Move disconnect telemetry to onSessionEnded so unpair cannot double-fire
or emit a phantom event, enable matching Device Connected, and add a
changelog fragment plus a DeviceProvider test for exact event counts.
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @undivisible — the PR-body Line-Count-Exception trailer did unblock the line-count ratchet (I confirmed it passes in the latest preflight run). Two notes on that trailer and the remaining blockers, which are otherwise unchanged on this head:

  1. The trailer's numbers don't match this PR. It claims 1655 -> 1679 | Device Connected/Disconnected telemetry plus test capture seam, but this diff takes AnalyticsManager.swift from 1655 to 1659 (4 added lines, disconnect-only — no connect-side tracking and no test capture seam). Please correct it to the actual counts and scope so the ledger stays auditable, e.g. Line-Count-Exception: desktop/macos/Desktop/Sources/AnalyticsManager.swift | 1655 -> 1659 | Device-disconnect analytics tracking.

  2. The one remaining red check is desktop-changelog-entry. Desktop production edits need a fragment under desktop/macos/changelog/unreleased/{"kind": "none"} is fine for internal-only telemetry.

The behavioral items from my earlier review still stand on 9aa1793: the double Device Disconnected emission when unpairing an active connection (once via onSessionEnded -> resetSessionPresentation, once via the direct call in unpair()), the phantom emission when unpairing an already-disconnected persisted device, deviceConnected still being commented out (DeviceProvider.swift:335) which leaves desktop denominator-only in the shared connection-balance metric, and the absence of any behavioral test or capture seam for the new emission path. One smaller consistency item: sibling methods in AnalyticsManager (e.g. screenCaptureBrokenDetected, line 564) guard with !Self.isDevBuild before emitting, while the new deviceDisconnected() (line 558) doesn't — worth aligning so dev builds don't pollute the production stream.

Leaving as changes-requested on the existing review. For the maintainer judgment call — whether desktop should join the shared Device Disconnected stream before deviceConnected parity lands — leaving for human maintainer review.


by AI on behalf of David — if you need David's attention urgently, please @Git-on-my-level and escalate with need human response.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6a62ed9a-69d3-44bc-90f0-22831d185d57)

@undivisible

Copy link
Copy Markdown
Collaborator Author

format drift on DeviceProviderTests.swift, Device Disconnected once per session is in d8eebb5 + this format commit.

@Git-on-my-level
Git-on-my-level dismissed their stale review September 2, 2026 04:39

Dismissing as resolved on 140cbfe: disconnect telemetry now emits once per ended session from onSessionEnded only (no double emission on unpair, no phantom emission on unpair of an already-disconnected device), deviceConnected is enabled, the Line-Count-Exception trailer matches the real 1655 -> 1679 growth, a valid kind:none changelog fragment exists, and testDisconnectAndUnpairEmitDeviceDisconnectedOnce pins the exact event counts. A fresh review covers new findings on this head.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @undivisible — this head resolves everything raised in the earlier reviews, and the emission design is now right. Verified on 140cbfe:

  • DeviceProvider.swift: the deviceDisconnected() call now lives in the coordinator.onSessionEnded closure (~line 152) and resetSessionPresentation() no longer emits, so unpair of an active connection fires exactly once, and unpair of an already-disconnected device fires nothing (DeviceSessionCoordinator.unpair() only invokes onSessionEnded inside its if let connection branch, DeviceSessionCoordinator.swift:285-296). deviceConnected is enabled at line 339. The PR-body Line-Count-Exception trailer now matches the real growth (1655 -> 1679, verified), and the {"kind":"none"} fragment satisfies check-desktop-changelog.py.
  • DeviceProviderTests.swift: testDisconnectAndUnpairEmitDeviceDisconnectedOnce pins the exact event sequence for connect -> disconnect, connect -> unpair, and unpair-of-disconnected (expects no event), with the new capture seam reset in tearDown. This is precisely the coverage the earlier reviews asked for.

Two things before merge, both small:

1. The macOS Device Connected payload doesn't match the contract in the docs row this PR edits. web/admin/docs/posthog-events.md now lists macOS as an owning surface for Device Connected with "Event: closed device_vendor, enum-name type; … Person: device_vendor". But AnalyticsManager.deviceConnected (AnalyticsManager.swift:566-575) sends device_type + device_name, no device_vendor, and no person property. Concretely:

  • the vendor-sliced weekly analysis the row links to cannot segment macOS events by vendor;
  • device_name is free text (user-renamable BLE name), while that file's maintenance contract keeps dimensions bounded/content-free and excludes device names from the properties documented there;
  • the sibling devicePairingReady (AnalyticsManager.swift:531-537) already shows the intended shape: device.type.analyticsVendorSlug + device.type.rawValue.

Mirroring that bounded set (device_vendor, device_type) and dropping device_name would make the code match the row this PR just edited; alternatively, if macOS deliberately needs different properties, the row should describe the macOS payload explicitly instead of inheriting the mobile contract.

2. No CI has run on the last two commits. Desktop Swift CI was last green on 9aa1793, which predates the emission move and the new test; d8eebb5 and 140cbfe have no workflow runs at all. The PR description mentions a local test.sh run — please make sure the Desktop Swift CI lane runs on this head before merge.

Non-blocking observation: deviceConnected fires at the end of the happy path (after checkFirmwareUpdates), so a session that becomes active at the coordinator level but fails post-connect setup will still emit Device Disconnected on teardown without a preceding Device Connected — a small downward skew on the connected/disconnected ratio in that edge. Fine to leave as is; worth knowing when calibrating the ratio alert.

The payload-shape choice in (1) is the one piece that needs a maintainer decision, since it defines the shared connection-funnel contract; everything else is mechanical.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level removed the needs-tests PR introduces logic that should be covered by tests label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-accuracy Documentation or committed reports need accuracy fixes macOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants