Repository navigation
fix(api): attribute purchase events to users - #3151
Conversation
PostHog payment conversion events (credits_purchased, dev_plan_started, chat_plan_started, reset_pass_purchased) were all captured against the literal distinct id "organization", piling every purchase onto one pseudo-person. Capture them against the purchasing user's id instead (the same id the frontends pass to posthog.identify), resolved from checkout metadata or the org billing email, so pageview journeys can be joined to payments in analytics. Falls back to "organization" when no user can be resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FZGskXGCobGxpJ6minAgpZ
Extend the Discord traffic report with: - Reset passes in the conversions table - Top 10 most visited pages - Top 5 converting pages (viewed in the 30 days before a person's first signup/payment of the period, with payer and signup counts) - Top blog posts with readers, signups, and payers they drove Payer attribution relies on payment events being captured against the purchasing user's person, fixed in the previous commit; counts fill in as new purchases are ingested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FZGskXGCobGxpJ6minAgpZ
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughStripe analytics now attribute purchase events to resolved purchaser IDs. Traffic reports now include top-page, conversion-attributed page, and blog conversion metrics in Discord embeds. ChangesStripe purchaser attribution
Traffic conversion reporting
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant fetchReport
participant PostHogHogQL
participant ReportData
participant buildEmbed
participant Discord
fetchReport->>PostHogHogQL: Execute traffic and conversion queries
PostHogHogQL-->>fetchReport: Return page and blog metrics
fetchReport->>ReportData: Map query results
ReportData->>buildEmbed: Provide report arrays
buildEmbed->>Discord: Render traffic tables
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
apps/api/src/stripe.ts (2)
1768-1792: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueDeduplicate emails to prevent redundant database queries.
If
userEmailandorganization.billingEmailhappen to be identical, and no user exists for that email, this function will query the database twice for the exact same address. You can optimize this by deduplicating the list of emails before iterating.♻️ Proposed refactor to deduplicate emails
async function resolvePurchaserDistinctId( ...emails: (string | null | undefined)[] ): Promise<string> { - for (const email of emails) { - if (!email) { - continue; - } + const uniqueEmails = new Set<string>(); + for (const email of emails) { + if (email) uniqueEmails.add(email); + } + + for (const email of uniqueEmails) { const purchaser = await db.query.user.findFirst({ where: { email: { eq: email } }, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/stripe.ts` around lines 1768 - 1792, Update resolvePurchaserDistinctId to deduplicate the provided email arguments before querying db.query.user.findFirst, while preserving the existing null/undefined filtering, purchaser-id return behavior, and "organization" fallback.
1411-1414: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove redundant type assertion.
The
as string | undefinedassertion is unnecessary becausemetadata?.userEmailis already correctly inferred asstring | undefinedfrom theStripe.Metadatatype.
apps/api/src/stripe.ts#L1411-L1414: remove theas string | undefinedcast frommetadata?.userEmail.apps/api/src/stripe.ts#L1536-L1539: remove theas string | undefinedcast frommetadata?.userEmail.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/stripe.ts` around lines 1411 - 1414, Remove the redundant string-or-undefined type assertion from metadata?.userEmail in both resolvePurchaserDistinctId call sites: apps/api/src/stripe.ts lines 1411-1414 and 1536-1539. Preserve the existing argument flow and rely on Stripe.Metadata’s inferred type.packages/scripts/src/traffic-report.ts (1)
357-370: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing the shared host list instead of hardcoding
'llmgateway.io'.Every other query filters by
properties.$host IN (${HOST_LIST}), butblogConversionsQueryhardcodese.properties.$host = 'llmgateway.io'. Not a bug today, but if the blog host constant already exists elsewhere (e.g. tied toPRODUCTS), referencing it here would keep the domain in one place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/scripts/src/traffic-report.ts` around lines 357 - 370, The blogConversionsQuery host filter currently hardcodes llmgateway.io; update it to reuse the existing shared host list or blog host constant used by the other traffic queries, preserving the existing pathname and aggregation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/scripts/src/traffic-report.ts`:
- Around line 83-96: Add "reset_pass_purchased" to the PAYMENT_EVENTS list so
convertingPages and blogConversions include reset-pass buyers; leave the
existing PAYMENT_EVENT_LIST generation unchanged.
- Around line 327-355: Bound attribution pageviews per person to
ATTRIBUTION_LOOKBACK_DAYS before that person’s first conversion, rather than
using the shared lookbackStart alone. Update the timestamp predicates in both
convertingPagesQuery and blogConversionsQuery to apply the corresponding
signed_up_at or paid_at lower bound while preserving the existing report-period
and conversion conditions.
---
Nitpick comments:
In `@apps/api/src/stripe.ts`:
- Around line 1768-1792: Update resolvePurchaserDistinctId to deduplicate the
provided email arguments before querying db.query.user.findFirst, while
preserving the existing null/undefined filtering, purchaser-id return behavior,
and "organization" fallback.
- Around line 1411-1414: Remove the redundant string-or-undefined type assertion
from metadata?.userEmail in both resolvePurchaserDistinctId call sites:
apps/api/src/stripe.ts lines 1411-1414 and 1536-1539. Preserve the existing
argument flow and rely on Stripe.Metadata’s inferred type.
In `@packages/scripts/src/traffic-report.ts`:
- Around line 357-370: The blogConversionsQuery host filter currently hardcodes
llmgateway.io; update it to reuse the existing shared host list or blog host
constant used by the other traffic queries, preserving the existing pathname and
aggregation behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 2ff474d6-19eb-4be5-8f24-325657c4e291
📒 Files selected for processing (2)
apps/api/src/stripe.tspackages/scripts/src/traffic-report.ts
- Count reset_pass_purchased as a payment event in conversion attribution - Bound attributed pageviews to the lookback window before each person's own conversion instead of the shared period lookback - Name the blog host constant instead of inlining it - Dedupe candidate emails in resolvePurchaserDistinctId and drop redundant type assertions on Stripe metadata Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FZGskXGCobGxpJ6minAgpZ
## Summary
Four growth/measurement workstreams driven by last week's traffic report
analysis:
### 1. Navbar: surface the most-clicked destinations (+ fix broken
dropdowns)
- **DevPass**, **Chat**, and **Models** are now direct navbar links (per
PostHog 30d traffic: DevPass home 2,949 visitors/mo, Models 2,352, Chat
926), no hover needed. Dropdowns remain for the long tail; mobile menu
updated to match.
- Responsive budget at the 1192px `nav:` breakpoint: Chat shows ≥1360px,
GitHub-stars/Discord cluster shows ≥1280px.
- All nav links (top-level + dropdown items) fire `nav_link_clicked`, so
future nav decisions can use real click data.
- **Fixes a dropdown regression that is live on production**:
`NavigationMenuLink`'s `flex-col` base class (concatenated via Radix
Slot) stacked every dropdown card vertically, blowing panels up to
~650px tall. One load-bearing `flex-row` restores the layout (~430px).
Panels also now center under their triggers instead of clipping off the
right viewport edge on <1600px screens.
### 2. Blog → signup CTAs
- New `BlogCta` component with a passport/boarding-pass DevPass variant
and a generic gateway variant, registered as a `markdown-to-jsx`
override — any post can embed `<BlogCta variant="devpass" />`.
- Embedded twice in `best-ai-coding-plans` (top post: 328 readers/wk, 2
signups). Clicks fire `cta_clicked` with `location`/`cta`/`post`; links
carry UTM params for per-post conversion attribution.
### 3. Analytics reliability
- **Bounded PostHog init** in ui + playground: captures fired before the
previously-unbounded idle-deferred `init()` were silently dropped — the
cause of `playground_chat_sent` zero-event days.
- **First-party `/ingest` proxy** (Next rewrites → `us.i.posthog.com`)
so ad blockers stop eating client events. Expect a one-time step **up**
in client-event counts after deploy — recovered data, not growth.
- **Weekly traffic report**: `cta_clicked`/`pricing_plan_clicked` now
count unique persons ("CTA clickers"/"Pricing clickers") so one bot
can't skew a week (708 pricing clicks from a single crawler on Jul 11).
- Payer attribution (0 payers on every page in the weekly report) needed
no change here — root cause was fixed by theopenco#3151.
### 4. Reset Pass upsell funnel instrumentation
- `devpass_weekly_cap_hit_viewed` (dashboard, on exhausted weekly
premium meter) with tier/usage/passes-available properties — the funnel
top for `reset_pass_purchased`.
- `reset_pass_redeemed` captured server-side in the redeem route
(previously emitted nothing).
- Follow-ups after deploy: build the cap-hit → purchase funnel + cap-hit
trend insights in PostHog; SQL gap-check in ~2 weeks to size the
never-visits-dashboard cohort before deciding on gateway-side capture +
cap-hit lifecycle email.
## Testing
- `turbo run build` passes for ui, playground, code, api, and scripts.
- All 32 `dev-plans-reset-passes` unit tests pass.
- Navbar verified via Playwright screenshots at 1200/1440/1550px (light
+ dark); dropdown panel bounds verified on-screen for
Products/Resources/AI at 1200px and 1550px; blog CTAs verified in light
+ dark.
https://claude.ai/code/session_01FYyCRLR4CU4FWoiSi7hd1K
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added reusable promotional cards to blog content, including links for
creating an account, browsing models, and exploring DevPass plans.
- Added contextual calls to action after rankings and near the end of
relevant articles.
- Improved navigation links across desktop and mobile layouts, including
access to DevPass, Chat, Models, Docs, and Pricing.
- **Analytics**
- Improved measurement of navigation, promotional interactions,
usage-limit views, and plan activity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
PostHog payment conversion events (credits_purchased,
dev_plan_started, chat_plan_started, reset_pass_purchased) were all
captured against the literal distinct id "organization", piling every
purchase onto one pseudo-person. Capture them against the purchasing
user's id instead (the same id the frontends pass to
posthog.identify), resolved from checkout metadata or the org billing
email, so pageview journeys can be joined to payments in analytics.
Falls back to "organization" when no user can be resolved.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FZGskXGCobGxpJ6minAgpZ
Summary by CodeRabbit
Analytics
Traffic Reports