fix(insights): backfill subscriptionTier from PostHog person props

inferSubscriptionTier only checked subscription_tier|tier|plan_tier|plan,
but Sase.tr's setPeopleProperties writes subscription_plan. Result: every
authed user showed tier=— in /insights/sessions/* and the tagger's
userValue boost (full=1.0, brand=0.7) collapsed to the 0.5 default.

- Add subscription_plan to the key list (priority first since it matches
  Sase.tr's actual property name)
- Extract into shared extractTierFromProps(props) helper
- In tag-sessions, re-evaluate tier from enrichment.userProperties and
  backfill when null. Tag step runs ~2min after ingest, by which point
  PostHog has the user's post-signup $set props that weren't on the
  recording row at ingest time.
This commit is contained in:
Semih
2026-05-20 17:06:12 +03:00
parent 16695de6d2
commit 42096814e3
2 changed files with 26 additions and 4 deletions

View File

@@ -1,7 +1,7 @@
import { prisma } from "../db";
import { tagSession, scoreSession } from "../lib/tagger";
import { buildEnrichment } from "../lib/enrich";
import { customEventPromoteReasons } from "../lib/heuristic";
import { customEventPromoteReasons, extractTierFromProps } from "../lib/heuristic";
import { getRecording } from "../lib/posthog";
const MIN_SCORE_FOR_COMPRESSION = Number(process.env.INSIGHT_MIN_SCORE ?? "30");
@@ -56,6 +56,13 @@ export async function runTagSessions(): Promise<{ tagged: number; discarded: num
const extraReasons = customEventPromoteReasons(eventNames);
const mergedReasons = Array.from(new Set([...s.promotionReasons, ...extraReasons]));
// Backfill subscriptionTier from the re-fetched person properties.
// Ingest only sees properties that existed at recording-list time; the
// tag step runs later (cron@2min) by which point PostHog has the user's
// post-signup $set props. Don't overwrite an existing tier.
const inferredTier = extractTierFromProps(enrichment.userProperties);
const tierUpdate = !s.subscriptionTier && inferredTier ? { subscriptionTier: inferredTier } : {};
const result = tagSession(s, enrichment);
const score = scoreSession(s, result.severity, 0.5, 0.2, enrichment.groupProperties);
@@ -69,6 +76,7 @@ export async function runTagSessions(): Promise<{ tagged: number; discarded: num
customEventCount: enrichment.customEvents.length,
promotionReasons: mergedReasons,
groupKey: enrichment.groupKey,
...tierUpdate,
},
});
discarded++;
@@ -86,6 +94,7 @@ export async function runTagSessions(): Promise<{ tagged: number; discarded: num
customEventCount: enrichment.customEvents.length,
promotionReasons: mergedReasons,
groupKey: enrichment.groupKey,
...tierUpdate,
},
});
tagged++;

View File

@@ -65,9 +65,22 @@ export function inferAuthenticated(rec: PHRecordingListItem): boolean {
}
export function inferSubscriptionTier(rec: PHRecordingListItem): string | null {
const props = rec.person?.properties ?? {};
const v = props.subscription_tier ?? props.tier ?? props.plan_tier ?? props.plan;
return typeof v === "string" ? v : null;
return extractTierFromProps(rec.person?.properties ?? {});
}
// Shared between ingest (PostHog recording person props) and tag-step
// (re-fetched person props via enrichment). Sase.tr sets `subscription_plan`
// (and `subscription_status`, but status isn't a tier). Older code only
// checked `subscription_tier`/`tier`/`plan_tier`/`plan`, missing the actual
// property Sase.tr writes — so trial users showed up as tier=null forever.
export function extractTierFromProps(props: Record<string, unknown>): string | null {
const v =
props.subscription_plan ??
props.subscription_tier ??
props.plan_tier ??
props.tier ??
props.plan;
return typeof v === "string" && v.length > 0 ? v : null;
}
// Promote signals derived from custom events (PRD v1.2 Bölüm 5.5).