Skip to content

feat: forward audited mutations to PostHog - #6058

Open
simplesagar wants to merge 3 commits into
grw-66-pr1-growthsignals-corefrom
grw-66-pr2-stream-handler
Open

feat: forward audited mutations to PostHog#6058
simplesagar wants to merge 3 commits into
grw-66-pr1-growthsignals-corefrom
grw-66-pr2-stream-handler

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 👈 this PR
  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
  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

Second of a five-PR stack. Targets #6057, review that first.

Summary

Turns audit-log outbox events into gram_activity and joins the handler to the existing webhook fan-out in the gram streams process. No new subscription and no new infrastructure.

Three behaviours worth a reviewer's attention:

  • Filters on the audit_log. event-type prefix before decoding. The whole webhook firehose reaches this handler, so decoding every event would be wasted work. The prefix also means new audited subjects are covered as services add them, with no list here to extend.
  • Switches on the audit action, not the event type. The type is a coarse bucket: an MCP server creation and a tool-metadata edit publish under the same one. Only the action says which activity a record is.
  • Every ignore path acks. A returned error nacks the Pub/Sub message and redelivers it forever, so a nonsensical envelope, an undecodable payload, and an excluded action are all logged and acked instead. Emission cannot fail the message either.

Two wiring choices:

  • The existing PostHog client is reused rather than a second being constructed.
  • Enrichment reads the primary, not a replica. These events describe a write that happened milliseconds earlier, so replica lag would leave a just-created row unresolvable exactly when it is most interesting. The lookups are TTL-cached, so the cost is per active organization rather than per event.

The streams command previously had no site URL source, so an optional site-url flag is added, matching worker.go's optional form rather than start.go's required one, so an unset variable cannot fail startup.

Motivation

This is the step that makes the audit log's existing signal visible. Without it the taxonomy from #6057 has no producer.

Temporal actions/month: 0, scales with fixed. Rides the existing subscription; no workflows, schedules or activities are added.


Summary by cubic

Turns audit-log outbox events into gram_activity events in PostHog by joining a new growth-signals handler to the existing webhook fan-out in gram streams. This is the second step of the GRW-66 PostHog/Slack notifications revamp; no new subscription or infrastructure is added.

Handler behavior

  • Filters on the audit_log. event-type prefix before decoding, so the rest of the webhook firehose costs nothing.
  • Switches on the audit action, not the event type, since an MCP server creation and a metadata edit share one type bucket.
  • Acks every ignore path — malformed envelope, undecodable payload, excluded action — because a returned error nacks the message and redelivers it forever.
  • Logs excluded actions at debug with the event id, so intentional filtering is distinguishable from a missing signal.
  • Stamps the outbox event id as PostHog's deduplication key, so a sibling handler's failure redelivering the message doesn't create a duplicate Slack line.
  • Logs and drops emission failures so analytics never hold up the stream.

Wiring

  • Reuses the existing PostHog client rather than constructing a second one.
  • Enriches against the read replica with TTL-cached lookups; a lagging miss omits a slug property rather than dropping the event.
  • Adds an optional site-url flag to gram streams for dashboard deep links; when set, startup fails unless it is an absolute http(s) URL.

Written for commit 375d8ad. 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:32
@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: 375d8ad

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 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.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/growthsignals/handler.go
Comment thread server/cmd/gram/streams.go
Comment thread server/internal/growthsignals/handler.go
@simplesagar
simplesagar force-pushed the grw-66-pr2-stream-handler branch 2 times, most recently from 1c0db91 to ba87bdb Compare September 4, 2026 04:13
simplesagar and others added 2 commits September 3, 2026 21:23
Adds the stream handler that turns audit-log outbox events into
`gram_activity`, and joins it to the existing webhook fan-out in the
`gram streams` process. No new subscription and no new infrastructure.

The handler filters on the `audit_log.` event-type prefix before
decoding, because the whole webhook firehose reaches it and decoding
every event would be wasted work. It then switches on the audit action
rather than the event type: the type is a coarse bucket, and an MCP
server creation and a tool-metadata edit share one, so only the action
says which activity a record is.

Every ignore path acks. A returned error nacks the Pub/Sub message and
redelivers it forever, so an envelope that makes no sense, a payload
that will not decode, and an excluded action are all logged and acked
instead. Emission cannot fail the message either: the emitter logs and
drops, because analytics must never hold up the stream.

Temporal actions/month: 0 (rides the existing subscription).

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

The topic is at-least-once and this handler shares a message with its
siblings, so when any of them fails the whole message nacks and every
handler sees the event again. Without a stable key that is a duplicate
Slack line for something that happened once. Each activity now carries
the outbox event id as PostHog's deduplication key, which is stable
across redeliveries.

The site URL is now rejected unless it is an absolute http(s) URL.
`url.Parse` accepts a bare path or a custom scheme, and either produces a
link Slack rejects — a misconfiguration worth failing on at startup
rather than discovering one dead button at a time.

Excluded actions now log at debug with the event id, so intentional
filtering is distinguishable from a missing signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
Comment thread server/cmd/gram/streams.go Outdated
Comment on lines +503 to +521
// Growth activities enrich against the primary rather than the read
// replica: an audit event arrives within milliseconds of the write it
// describes, so replica lag would leave a just-created project or
// organization unresolvable exactly when it is most interesting.
var siteURL *url.URL
if raw := c.String("site-url"); raw != "" {
siteURL, err = url.Parse(raw)
if err != nil {
return fmt.Errorf("parse site url: %w", err)
}
// url.Parse accepts a bare path or a custom scheme, and either
// would produce a link Slack rejects. A site URL that cannot
// address the dashboard is a misconfiguration worth failing on
// rather than discovering one dead button at a time.
if (siteURL.Scheme != "http" && siteURL.Scheme != "https") || siteURL.Host == "" {
return fmt.Errorf("site url must be an absolute http(s) URL, got %q", raw)
}
}
growthSignalHandler := growthsignals.NewEventHandler(logger, growthsignals.NewEmitter(logger, posthogClient, growthsignals.NewDatabaseEnricher(db), siteURL))

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.

  • move emitter construction to deps.go as newGrowthSignalsEmitter
  • consider using the db read replica to relieve prod db. replication lag will not bite here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both done in 375d8ad. Construction moved to newGrowthSignalsEmitter in deps.go, and enrichment now reads the replica — agreed lag is harmless here, since a miss omits a display-name property rather than dropping the event.

Review feedback.

Emitter construction moves to newGrowthSignalsEmitter in deps.go,
alongside the other dependency constructors.

Enrichment moves to the read replica. The lookups are display names for
an ops notification, not authority for anything, and a miss degrades the
event by omitting a property rather than dropping it — so replication lag
costs at most a missing slug on a very fresh row, which is not worth the
primary's capacity.

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.

2 participants