Skip to content

fix(api): attribute purchase events to users - #3151

Merged
smakosh merged 3 commits into
mainfrom
claude/weekly-traffic-report-metrics-fghv6y
Jul 20, 2026
Merged

smakosh merged 3 commits into
mainfrom
claude/weekly-traffic-report-metrics-fghv6y

Conversation

@smakosh

@smakosh smakosh commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

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

    • Improved PostHog attribution for purchase/conversion events by using the purchaser’s user identity when available (instead of a generic fallback).
    • Included reset-pass purchases in payment/conversion tracking.
  • Traffic Reports

    • Reports now show top pages by views.
    • Added “top converting pages” (attributed signups/payers using a lookback window).
    • Added blog post readership plus attributed signup/payer metrics.

claude added 2 commits July 20, 2026 10:14
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
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 584a310d-e1f5-4f1f-aae7-78cdccc67aab

📥 Commits

Reviewing files that changed from the base of the PR and between 27d6172 and 6b57b36.

📒 Files selected for processing (2)
  • apps/api/src/stripe.ts
  • packages/scripts/src/traffic-report.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/stripe.ts
  • packages/scripts/src/traffic-report.ts

Walkthrough

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

Changes

Stripe purchaser attribution

Layer / File(s) Summary
Purchaser resolution and event attribution
apps/api/src/stripe.ts
Purchase events resolve distinct IDs from purchaser emails or metadata, falling back to the organization identity.
Credit top-up attribution
apps/api/src/stripe.ts
Credit top-up handlers pass resolved purchaser IDs into analytics events, with email-based fallback resolution.

Traffic conversion reporting

Layer / File(s) Summary
Traffic report data contracts
packages/scripts/src/traffic-report.ts
Adds typed page and conversion result fields, reset-pass payment events, and attribution configuration.
Page and conversion queries
packages/scripts/src/traffic-report.ts
Queries and maps top pages, attributed converting pages, and blog conversion results using the configured lookback window.
Discord traffic report rendering
packages/scripts/src/traffic-report.ts
Renders the new metrics as Discord tables with truncated paths and empty-state text.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: attributing purchase events to users instead of organization IDs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/weekly-traffic-report-metrics-fghv6y

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/api/src/stripe.ts (2)

1768-1792: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Deduplicate emails to prevent redundant database queries.

If userEmail and organization.billingEmail happen 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 value

Remove redundant type assertion.

The as string | undefined assertion is unnecessary because metadata?.userEmail is already correctly inferred as string | undefined from the Stripe.Metadata type.

  • apps/api/src/stripe.ts#L1411-L1414: remove the as string | undefined cast from metadata?.userEmail.
  • apps/api/src/stripe.ts#L1536-L1539: remove the as string | undefined cast from metadata?.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 value

Consider reusing the shared host list instead of hardcoding 'llmgateway.io'.

Every other query filters by properties.$host IN (${HOST_LIST}), but blogConversionsQuery hardcodes e.properties.$host = 'llmgateway.io'. Not a bug today, but if the blog host constant already exists elsewhere (e.g. tied to PRODUCTS), 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

📥 Commits

Reviewing files that changed from the base of the PR and between a31f188 and 27d6172.

📒 Files selected for processing (2)
  • apps/api/src/stripe.ts
  • packages/scripts/src/traffic-report.ts

Comment thread packages/scripts/src/traffic-report.ts
Comment thread packages/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
@smakosh
smakosh enabled auto-merge July 20, 2026 10:48
@smakosh
smakosh added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit 52c1701 Jul 20, 2026
12 checks passed
@smakosh
smakosh deleted the claude/weekly-traffic-report-metrics-fghv6y branch July 20, 2026 11:06
pull Bot pushed a commit to soitun/llmgateway that referenced this pull request Jul 21, 2026
## 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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants