Skip to content

feat: report devices appearing in a fleet - #6060

Open
simplesagar wants to merge 2 commits into
grw-66-pr3-signup-sourcefrom
grw-66-pr4-devices
Open

feat: report devices appearing in a fleet#6060
simplesagar wants to merge 2 commits into
grw-66-pr3-signup-sourcefrom
grw-66-pr4-devices

Conversation

@simplesagar

@simplesagar simplesagar commented Sep 4, 2026

Copy link
Copy Markdown
Member

Note

Stacked PR — merge bottom-up. Each PR targets the one above it, so its Files tab shows only its own diff.

  1. feat: add the growthsignals activity taxonomy and emitter #6057 — core taxonomy and emitter
  2. feat: forward audited mutations to PostHog #6058 — audit stream to PostHog
  3. feat: report signups as invited or organic #6059 — direct emits: signup source, org created, member joined
  4. feat: report devices appearing in a fleet #6060 — devices 👈 this PR
  5. feat: report agents new to an organization #6061 — agents

Merging #6057 retargets #6058 to main automatically, and so on down the stack.

GRW-66

Fourth of a five-PR stack. Targets #6059, review that first.

Summary

Emits device_first_seen when an MDM snapshot inserts a device the inventory has not held before.

Detecting that at all required a query change. UpsertMdmDevice returned the same row count on insert and on update, so a first sighting and a re-sighting were indistinguishable. It now returns (xmax = 0), which is true only on a freshly inserted row. The config-guard case, which previously surfaced as a zero row count, is now no row at all and maps to the same stale-sync outcome as before, so that behaviour is unchanged.

Two guards keep this from flooding the channel:

  • A config's first successful sync is a backfill, not news. It inserts the whole existing fleet at once, so it reports nothing. last_poll_success_at is the signal, newly exposed on GetSyncTarget.
  • Later syncs report at most ten devices. A fleet that doubles overnight is worth one glance at the dashboard, not hundreds of notifications. The devices are all in the inventory either way.

Reporting happens only after the snapshot is durable. Emitting inside the loop would announce devices for a run that later aborts as stale, and every abort path returns before the emit.

These activities are organization-scoped, because no MDM table carries a project id.

Motivation

Devices were the riskiest item on the ticket precisely because none of the three device ingest paths could tell that a row was new, and the bulk-sync shape meant a naive implementation would announce an entire fleet the first time an integration was connected.

Temporal actions/month: 0, scales with fixed. No schedules, workflows or activities are added; this rides the existing device-integration sync.


Summary by cubic

Emits device_first_seen when an MDM snapshot inserts a device the inventory hasn't held before, covering the devices scope of GRW-66. The upsert previously returned the same row count on insert and update, so a first sighting and a re-sighting were indistinguishable.

  • UpsertMdmDevice now returns (xmax = 0), true only on a freshly inserted row; the config-guard case is now no row at all and maps to the same stale-sync outcome as before.
  • Two guards prevent flooding: a config's first successful sync is a backfill and reports nothing (via last_poll_success_at, newly exposed on GetSyncTarget), and later syncs report at most ten devices.
  • The device's assigned user travels as a device_owner_email property rather than the actor, so the sighting isn't attached to the owner's PostHog person.
  • Reporting is at-most-once: rows committed before a later-page failure read as existing on retry, so the retry reports nothing.
  • Reporting happens only after the snapshot is durable, so runs that later abort as stale announce nothing.
  • These activities are organization-scoped because no MDM table carries a project id.
  • No Temporal schedules, workflows, or activities are added; this rides the existing device-integration sync.
  • The design spec drops the MCP settings destination; those events ride the firehose instead.

Written for commit 9392ab5. Summary will update on new commits.

Review in cubic

@simplesagar
simplesagar requested a review from a team as a code owner September 4, 2026 03:49
@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GRW-66

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9392ab5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@simplesagar simplesagar added the enhancement New feature or request label Sep 4, 2026
@cubic-dev-ai

cubic-dev-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Running ultrareview automatically — This changes the MDM upsert to rely on Postgres xmax to distinguish inserts from updates, and a subtle bug here could break stale-sync detection and merge stale inventory into new configs, risking data integrity.. I'll post findings when complete.

@blacksmith-sh

This comment has been minimized.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ultrareview completed in 4m 51s

All reported issues were addressed across 8 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread server/internal/background/activities.go
Comment thread server/internal/deviceintegrations/queries.sql
Comment thread server/internal/deviceintegrations/sync.go Outdated
Comment thread server/internal/deviceintegrations/sync.go
simplesagar and others added 2 commits September 4, 2026 12:32
Emits `device_first_seen` when an MDM snapshot inserts a device the
inventory has not held before.

Detecting that at all needed a query change. The upsert returned the same
row count on insert and on update, so a first sighting and a re-sighting
were indistinguishable; it now returns `(xmax = 0)`, which is zero only
on a freshly inserted row. The config-guard case, which previously showed
up as a zero row count, is now no row at all and maps to the same stale
sync outcome as before.

Two guards keep this from flooding the channel. A config that has never
completed a sync is being backfilled, so its first snapshot inserts the
whole existing fleet and reports none of it. Later syncs report at most
ten devices, because a fleet that doubles overnight is worth one glance
at the dashboard rather than hundreds of notifications.

Reporting happens only once the snapshot is durable. Emitting inside the
loop would announce devices for a run that later aborts as stale.

These activities are organization-scoped: no MDM table carries a project.

Temporal actions/month: 0 (no schedules, workflows or activities added;
this rides the existing device-integration sync).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
…owner

Three findings from review.

The MDM-assigned user is a device's owner, not the actor. Reporting them
as the actor attributed the sighting to them and attached it to their
PostHog person, when nobody performed it: a scheduled sync observed it.
The owner now travels as `device_owner_email`.

The guard-refusal regression test asserted the old contract. A refused
write used to be a zero row count and is now no row at all, so the test
asserts `pgx.ErrNoRows`.

Documents that reporting is at-most-once. A sync that inserts rows and
then fails on a later page has already committed them, so the retry sees
them as existing and reports nothing. Persisting pending sightings would
buy durability for an ops notification at the cost of a table and its own
failure modes; the devices are in the inventory either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant