Mechanically re-derived: bin/debrand runs over a fresh import of posthog/posthog@fa1a9a60d4 (2026-08-06). The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md and the squashed initial migration are restored from the shipped tree, same as the previous import. The prune's import-closure guard kept 21 products the new core now imports; dropping them is a refactor now, recorded rather than forced.
4.9 KiB
name, description
| name | description |
|---|---|
| querying-canvas-data | Get Insights data into a canvas correctly: the host-injected `ph` SDK (loadInsight, query, capture, openExternal, navigate), the data hierarchy (saved insights first, typed query nodes second, inline InsightsQL last), per-insight-type result shapes, date-range wiring, and event capture from a canvas. Use whenever a canvas shows metrics, charts, tables, or any Insights data, or needs to send analytics events. |
Querying canvas data
The global ph object (injected by the host — never imported, never initialized) is the only way
a canvas talks to Insights. Credentials stay in the host; fetch(), insights-js, and hand-rolled
clients fail in the sandbox.
Data hierarchy — back every metric with a saved insight
- Preferred — save an insight, load it by reference. Use the Insights MCP insight tools to
create/save an insight that computes the metric with an insight query type (TrendsQuery,
FunnelsQuery, RetentionQuery, PathsQuery, or the web-analytics kinds WebOverviewQuery /
WebStatsTableQuery — not raw SQL). Confirm its numbers, note the
short_id, and render it withawait ph.loadInsight(shortId, { dateRange }). These are proven queries — numbers match the Insights UI exactly (sessionization, unique users, breakdowns, bounce rate). Never fabricate a query or guess event/property names; discover and save them via MCP first. - Secondary — an ad-hoc typed node:
ph.query({ kind: "TrendsQuery", series: [...], dateRange: {...} })when saving an insight genuinely doesn't fit. - Last resort — inline InsightsQL:
ph.query("SELECT …"), only when no insight kind can express the metric; you then own the SQL and its date window.
For web-analytics boards specifically, use the web-analytics query kinds — raw InsightsQL subtly gets bounce rate, sessionization, channel attribution, and unique-visitor counts wrong.
Whatever tier you use, declare it in the project's capabilities before publishing: every
ph.loadInsight short id in capabilities.insights.insights, every ph.capture event name in
captureEvents, and inlineQueries: true for any ph.query use. The host rejects undeclared
calls at runtime, and validation fails on undeclared literals.
Result shapes — read them correctly or every value renders 0
- Trends-style results (insight query types, via
ph.loadInsightor a typed node):resultsis an array of series objects, not rows. Each series hasdata: number[](per interval),days: string[](ISO),labels: string[],count(sum),aggregated_value(single-value total),label, and optionalcompare_label: "current" | "previous". A KPI total isresults[0].count(or.aggregated_value); a line chart plotsresults[0].dataoverresults[0].days. With a compare period, find the prior series bycompare_label === "previous"— never by index.columnsis empty here. - SQL results:
{ columns: string[], results: rows[][] }— each row an array of cell values incolumnsorder.
Load data in useEffect with useState, show a loading state, and aggregate in the query; never
fetch raw event dumps. Treat a rejected query and an empty result as different states: .catch
must set an error state that renders visibly (message + retry), never fall through to zeros, an
empty chart, or a "no data" message — a swallowed error makes real breakage (a missing table, an
auth failure) look like missing data. Reserve the empty state for a query that succeeded with no
rows.
Date windows
- Pass the canvas's date-picker window straight into
dateRange:ph.loadInsight(shortId, { dateRange: { date_from: win.start.toISOString(), date_to: win.end.toISOString() } })— the saved insight re-scopes to the window with no time SQL. Typed nodes take the samedateRange. Re-run every query when the window changes. - A saved SQL insight may ignore
dateRange(its window lives inside the SQL) — a reason to prefer insight query types. - Inline InsightsQL escape hatch only: never bake
now()or a hardcoded INTERVAL. Compute unix bounds (Math.floor(win.start.getTime() / 1000)) and write half-opentimestamp >= toDateTime(fromUnix) AND timestamp < toDateTime(toUnix). Prior period = the equal-length window immediately before; bucket withtoStartOfDay/toStartOfHour.
Side effects
ph.capture(event, properties?, distinctId?)— analytics events for interactions (fire-and-forget). Session replay,$session_id, and person attribution are handled by the host automatically; never roll your own capture.ph.openExternal(url)— openshttps://hanzo.ai/*.hanzo.aiURLs only, and only from a user interaction (opens outside focus are ignored). Don't link elsewhere.ph.navigate.toTask(id)/.toNewTask()/.toCanvas(id)/.toNewCanvas()— in-app navigation within the canvas's own channel.