Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@ import {
ChartTooltip,
ChartTooltipContent,
} from "@superset/ui/chart";
import { useQuery } from "@tanstack/react-query";
import { Bar, ComposedChart, Line, XAxis, YAxis } from "recharts";
import { useTRPC } from "@/trpc/react";

import { useInsightResults } from "../../hooks/useInsightResults";
import { makeDateAxis } from "../../utils/chartAxis";
import { InsightTileFrame } from "../InsightTileFrame";

const STALE_TIME_MS = 10 * 60 * 1000;

// Renders a saved HogQL (DataVisualizationNode) insight whose result is an
// array of rows. Column order is part of the canonical definition in PostHog,
// so series are addressed by column index.
Expand Down Expand Up @@ -47,13 +44,7 @@ export function HogQLLineTile({
xColumn,
series,
}: HogQLLineTileProps) {
const trpc = useTRPC();
const query = useQuery(
trpc.analytics.getInsightResults.queryOptions(
{ insight },
{ staleTime: STALE_TIME_MS },
),
);
const query = useInsightResults(insight);

const rows = Array.isArray(query.data?.result)
? (query.data.result as unknown[][]).filter(Array.isArray)
Expand Down Expand Up @@ -82,7 +73,7 @@ export function HogQLLineTile({
title={query.data?.name ?? insight}
description={description}
lastRefresh={query.data?.lastRefresh}
isLoading={query.isLoading}
isLoading={query.isLoading || query.data?.result == null}
error={query.error}
href={`${POSTHOG_PROJECT_URL}/insights/${ADMIN_INSIGHTS[insight]}`}
onRefresh={() => query.refetch()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import {
} from "@superset/trpc/insight-registry";
import { Button } from "@superset/ui/button";
import { cn } from "@superset/ui/utils";
import { useQuery } from "@tanstack/react-query";
import { LuExternalLink, LuRefreshCw } from "react-icons/lu";

import { useTRPC } from "@/trpc/react";

import { useInsightResults } from "../../hooks/useInsightResults";
import { FunnelChart } from "../FunnelChart";

const STALE_TIME_MS = 10 * 60 * 1000;

interface PostHogFunnelStep {
name: string;
custom_name?: string | null;
Expand All @@ -31,13 +27,7 @@ const STEP_NAME_OVERRIDES: Record<number, string> = {
};

export function PostHogFunnelTile() {
const trpc = useTRPC();
const insight = useQuery(
trpc.analytics.getInsightResults.queryOptions(
{ insight: "activationFunnel" },
{ staleTime: STALE_TIME_MS },
),
);
const insight = useInsightResults("activationFunnel");

const steps = Array.isArray(insight.data?.result)
? (insight.data.result as PostHogFunnelStep[])
Expand All @@ -54,7 +44,7 @@ export function PostHogFunnelTile() {
title={insight.data?.name ?? "New-user activation"}
description="First sign-in view → auth → onboarding → real workspace (last 7d, 2d window)"
steps={data}
isLoading={insight.isLoading}
isLoading={insight.isLoading || insight.data?.result == null}
error={insight.error}
headerAction={
<div className="flex items-center gap-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import {
ADMIN_INSIGHTS,
POSTHOG_PROJECT_URL,
} from "@superset/trpc/insight-registry";
import { useQuery } from "@tanstack/react-query";

import { useTRPC } from "@/trpc/react";

import { useInsightResults } from "../../hooks/useInsightResults";
import { formatDay } from "../../utils/chartAxis";
import {
type CohortCellState,
Expand All @@ -16,7 +13,6 @@ import {
} from "../CohortGrid";
import { InsightTileFrame } from "../InsightTileFrame";

const STALE_TIME_MS = 10 * 60 * 1000;
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;

interface RetentionCohortResult {
Expand All @@ -37,13 +33,7 @@ function cellState(
}

export function RetentionGridTile() {
const trpc = useTRPC();
const query = useQuery(
trpc.analytics.getInsightResults.queryOptions(
{ insight: "cohortRetention" },
{ staleTime: STALE_TIME_MS },
),
);
const query = useInsightResults("cohortRetention");

const cohorts = Array.isArray(query.data?.result)
? (query.data.result as RetentionCohortResult[]).filter((c) =>
Expand Down Expand Up @@ -107,7 +97,7 @@ export function RetentionGridTile() {
title={query.data?.name ?? "Cohort retention"}
description="Weekly cohorts by first real workspace; % returning with another workspace each week"
lastRefresh={query.data?.lastRefresh}
isLoading={query.isLoading}
isLoading={query.isLoading || query.data?.result == null}
error={query.error}
empty={cohorts.length === 0}
href={`${POSTHOG_PROJECT_URL}/insights/${ADMIN_INSIGHTS.cohortRetention}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,12 @@ import {
ChartTooltip,
ChartTooltipContent,
} from "@superset/ui/chart";
import { useQuery } from "@tanstack/react-query";
import { Line, LineChart, XAxis, YAxis } from "recharts";

import { useTRPC } from "@/trpc/react";

import { useInsightResults } from "../../hooks/useInsightResults";
import { makeDateAxis } from "../../utils/chartAxis";
import { InsightTileFrame } from "../InsightTileFrame";

const STALE_TIME_MS = 10 * 60 * 1000;

// Renders TrendsQuery / funnel-trends insight results: an array of series,
// each carrying parallel `data` values and `labels`/`days` for the x axis.
interface TrendSeries {
Expand Down Expand Up @@ -48,13 +44,7 @@ export function TrendSeriesTile({
valueSuffix,
dashIncompleteLast,
}: TrendSeriesTileProps) {
const trpc = useTRPC();
const query = useQuery(
trpc.analytics.getInsightResults.queryOptions(
{ insight },
{ staleTime: STALE_TIME_MS },
),
);
const query = useInsightResults(insight);

const series = Array.isArray(query.data?.result)
? (query.data.result as TrendSeries[]).filter((s) => Array.isArray(s.data))
Expand Down Expand Up @@ -95,7 +85,7 @@ export function TrendSeriesTile({
title={query.data?.name ?? insight}
description={description}
lastRefresh={query.data?.lastRefresh}
isLoading={query.isLoading}
isLoading={query.isLoading || query.data?.result == null}
error={query.error}
href={`${POSTHOG_PROJECT_URL}/insights/${ADMIN_INSIGHTS[insight]}`}
onRefresh={() => query.refetch()}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useInsightResults } from "./useInsightResults";
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import type { AdminInsightKey } from "@superset/trpc/insight-registry";
import { useQuery } from "@tanstack/react-query";

import { useTRPC } from "@/trpc/react";

const STALE_TIME_MS = 10 * 60 * 1000;
const PENDING_POLL_MS = 3 * 1000;

// PostHog returns its cached result right away and recomputes stale insights in
// the background, so a cold insight comes back `pending` with no result yet.
// Poll until it lands instead of holding the request open for the recompute.
export function useInsightResults(insight: AdminInsightKey) {
const trpc = useTRPC();
return useQuery(
trpc.analytics.getInsightResults.queryOptions(
{ insight },
{
staleTime: STALE_TIME_MS,
refetchInterval: (q) =>
q.state.data?.pending ? PENDING_POLL_MS : false,
},
),
);
}
30 changes: 27 additions & 3 deletions packages/trpc/src/lib/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ export interface InsightResults {
name: string;
shortId: string;
lastRefresh: string | null;
// PostHog is recomputing and had nothing cached to serve; the caller polls
// until a result lands.
pending: boolean;
result: unknown;
}

// No app-side cache: refresh=blocking serves PostHog's own result cache when
// fresh and recomputes only past its refresh throttle (~15 min).
// No app-side cache: refresh=lazy_async serves PostHog's own result cache and
// recomputes in the background past its refresh throttle (~15 min). It must
// stay async — the heavy HogQL insights take 15-20s to recompute, and
// refresh=blocking held the request open for all of it.
export async function fetchInsightResults(
shortId: string,
): Promise<InsightResults> {
const response = await fetch(
`${env.POSTHOG_API_HOST}/api/projects/${env.POSTHOG_PROJECT_ID}/insights/?short_id=${shortId}&refresh=blocking`,
`${env.POSTHOG_API_HOST}/api/projects/${env.POSTHOG_PROJECT_ID}/insights/?short_id=${shortId}&refresh=lazy_async`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{
headers: {
Authorization: `Bearer ${env.POSTHOG_API_KEY}`,
Expand All @@ -31,6 +36,11 @@ export async function fetchInsightResults(
name: string | null;
short_id: string;
last_refresh: string | null;
query_status: {
complete?: boolean;
error?: boolean;
error_message?: string | null;
} | null;
result: unknown;
}>;
};
Expand All @@ -40,10 +50,24 @@ export async function fetchInsightResults(
throw new Error(`PostHog insight not found: ${shortId}`);
}

const status = insight.query_status;

// A failed query is terminal — retrying means starting a new one, so polling
// this one would spin forever behind a skeleton. Surface it instead.
if (status?.error) {
throw new Error(
`PostHog insight ${shortId}: ${status.error_message ?? "query failed"}`,
);
}

return {
name: insight.name ?? "",
shortId: insight.short_id,
lastRefresh: insight.last_refresh,
// Past cache_target_age PostHog hands back the stale result *and* a live
// query_status for the recompute it just started, so a present result is
// not on its own a sign that there is nothing left to wait for.
pending: insight.result == null || (status != null && !status.complete),
result: insight.result,
};
}
Loading