Skip to content

feat(canvas): web analytics template + date-range picker#2657

Open
adamleithp wants to merge 1 commit into
mainfrom
feat/canvas-web-analytics
Open

feat(canvas): web analytics template + date-range picker#2657
adamleithp wants to merge 1 commit into
mainfrom
feat/canvas-web-analytics

Conversation

@adamleithp

Copy link
Copy Markdown
Contributor

What

A new Web analytics canvas template that drives the agent to build a PostHog-style board (KPI row, visitor trends with prior-period comparison, sources, geography, retention, active hours) for a selectable date range.

Highlights

  • New catalog components: Heatmap (day×hour activity) and RetentionGrid (cohort bars) — schema + bodies + registry wiring.
  • DateTimePicker in the canvas toolbar (quill), shown in view and edit:
    • Named ranges (e.g. "Last 7 days") roll to now; Custom pins to absolute from/to with a human-readable label.
    • View mode re-runs the board's queries for the window; edit mode feeds the agent's next build via a [Range] prompt line.
    • Optimistic label + spinner while applying; clears on success or failure (no stuck-disabled state).
  • Multi-value query refresh: queries declare a shape (scalar/column/labels/matrix/pairs/retention) mapped onto props via a JSON-pointer setter. Mis-shaped numeric columns fail (instead of charting zeros); identical values skip the file write (no poll churn).
  • Timezone-safe time windows: {date_from}/{date_to} (+ _prev for comparison series) substituted as UTC-instant toDateTime(<unix>) literals.
  • Channels remember collapsed state in localStorage (closed by default).
  • Tables use quill's stickyHeader with a capped scroll viewport; Filter button disabled (not wired yet).

Test plan

  • pnpm --filter @posthog/core test — 1415 pass (incl. new dashboardQueryService shape-mapping tests).
  • pnpm typecheck (core/ui/host-router) + biome lint clean.
  • Manual: create a Web analytics canvas, build it, change the date range in view (rolls + re-queries) and edit (feeds the agent).

🤖 Generated with Claude Code

@adamleithp adamleithp added the Stamphog This will request an autostamp by stamphog on small changes label Jun 13, 2026
@adamleithp adamleithp enabled auto-merge (squash) June 13, 2026 09:54
@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/core/src/canvas/dashboardQueryService.test.ts:29-108
**Non-parameterised shape tests**

The six happy-path shape cases (`scalar`, `column`, `labels`, `matrix`, `pairs`, `retention`) share the same fixture → run → `toMatchObject` structure and could be collapsed into a single `it.each` table. The two failure tests (lines 82-101) also share `expect(r.ok).toBe(false)` and are a natural second table. Keeping them as individual `it` blocks violates the project's "always prefer parameterised tests" convention and makes the list harder to extend when new shapes are added.

### Issue 2 of 2
packages/ui/src/features/canvas/components/ChannelsList.tsx:58-62
Closed channels write `"0"` to `localStorage`, but the comment above `useChannelOpen` says "only an explicit open is stored." The `"0"` entries are redundant because absent keys are also read as `false`; over many channels they accumulate needless storage entries. Remove the `"0"` write (or remove the stored key entirely on close) to match the documented intent.

```suggestion
      try {
        if (next) {
          localStorage.setItem(key, "1");
        } else {
          localStorage.removeItem(key);
        }
      } catch {
        // Ignore storage failures (private mode, quota) — state still works in-session.
      }
```

Reviews (1): Last reviewed commit: "feat(canvas): web analytics template + d..." | Re-trigger Greptile

Comment on lines +29 to +108
describe("DashboardQueryService shape mapping", () => {
it("scalar reads row 0, col 0", async () => {
const [r] = await serviceReturning([[42]]).run({
queries: [query("scalar")],
});
expect(r).toMatchObject({ ok: true, value: 42 });
});

it("column collects the first cell of every row", async () => {
const [r] = await serviceReturning([[1], [2], [3]]).run({
queries: [query("column")],
});
expect(r).toMatchObject({ ok: true, value: [1, 2, 3] });
});

it("labels stringifies the first column", async () => {
const [r] = await serviceReturning([["Jun 4"], ["Jun 5"]]).run({
queries: [query("labels")],
});
expect(r).toMatchObject({ ok: true, value: ["Jun 4", "Jun 5"] });
});

it("matrix keeps every row as an array", async () => {
const [r] = await serviceReturning([
["/", 10, 20],
["/pricing", 5, 8],
]).run({ queries: [query("matrix")] });
expect(r).toMatchObject({
ok: true,
value: [
["/", 10, 20],
["/pricing", 5, 8],
],
});
});

it("pairs maps rows to {label,value}", async () => {
const [r] = await serviceReturning([
["Direct", 5],
["Organic", 3],
]).run({ queries: [query("pairs")] });
expect(r).toMatchObject({
ok: true,
value: [
{ label: "Direct", value: 5 },
{ label: "Organic", value: 3 },
],
});
});

it("retention maps rows to {label,size,values}", async () => {
const [r] = await serviceReturning([["Jun 1", 100, 100, 9]]).run({
queries: [query("retention")],
});
expect(r).toMatchObject({
ok: true,
value: [{ label: "Jun 1", size: 100, values: [100, 9] }],
});
});

it("fails a scalar that isn't a string/number", async () => {
const [r] = await serviceReturning([[{ nested: true }]]).run({
queries: [query("scalar")],
});
expect(r.ok).toBe(false);
});

it("fails a column whose cells are non-numeric (mis-shaped query)", async () => {
const [r] = await serviceReturning([["Direct"], ["Organic"]]).run({
queries: [query("column")],
});
expect(r.ok).toBe(false);
});

it("treats null column cells as empty buckets (0), not a failure", async () => {
const [r] = await serviceReturning([[5], [null], [3]]).run({
queries: [query("column")],
});
expect(r).toMatchObject({ ok: true, value: [5, 0, 3] });
});

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.

P2 Non-parameterised shape tests

The six happy-path shape cases (scalar, column, labels, matrix, pairs, retention) share the same fixture → run → toMatchObject structure and could be collapsed into a single it.each table. The two failure tests (lines 82-101) also share expect(r.ok).toBe(false) and are a natural second table. Keeping them as individual it blocks violates the project's "always prefer parameterised tests" convention and makes the list harder to extend when new shapes are added.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/canvas/dashboardQueryService.test.ts
Line: 29-108

Comment:
**Non-parameterised shape tests**

The six happy-path shape cases (`scalar`, `column`, `labels`, `matrix`, `pairs`, `retention`) share the same fixture → run → `toMatchObject` structure and could be collapsed into a single `it.each` table. The two failure tests (lines 82-101) also share `expect(r.ok).toBe(false)` and are a natural second table. Keeping them as individual `it` blocks violates the project's "always prefer parameterised tests" convention and makes the list harder to extend when new shapes are added.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +58 to +62
try {
localStorage.setItem(key, next ? "1" : "0");
} catch {
// Ignore storage failures (private mode, quota) — state still works in-session.
}

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.

P2 Closed channels write "0" to localStorage, but the comment above useChannelOpen says "only an explicit open is stored." The "0" entries are redundant because absent keys are also read as false; over many channels they accumulate needless storage entries. Remove the "0" write (or remove the stored key entirely on close) to match the documented intent.

Suggested change
try {
localStorage.setItem(key, next ? "1" : "0");
} catch {
// Ignore storage failures (private mode, quota) — state still works in-session.
}
try {
if (next) {
localStorage.setItem(key, "1");
} else {
localStorage.removeItem(key);
}
} catch {
// Ignore storage failures (private mode, quota) — state still works in-session.
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/components/ChannelsList.tsx
Line: 58-62

Comment:
Closed channels write `"0"` to `localStorage`, but the comment above `useChannelOpen` says "only an explicit open is stored." The `"0"` entries are redundant because absent keys are also read as `false`; over many channels they accumulate needless storage entries. Remove the `"0"` write (or remove the stored key entirely on close) to match the documented intent.

```suggestion
      try {
        if (next) {
          localStorage.setItem(key, "1");
        } else {
          localStorage.removeItem(key);
        }
      } catch {
        // Ignore storage failures (private mode, quota) — state still works in-session.
      }
```

How can I resolve this? If you propose a fix, please make it concise.

@adamleithp adamleithp disabled auto-merge June 13, 2026 10:08
Canvas template system (Dashboard / Web analytics / Blank) with a create
picker, per-template agent prompts + component allow-lists, rename, the
Quill component pass, and the showcase seed — plus the Web analytics
template and its date-range picker.

- Templates: canvasTemplates + templateSchemas + CanvasTemplatesService,
  templateId on canvas records, NewCanvasMenu picker, per-template chat
  suggestions.
- Web analytics template: KPI row, visitor trends with prior-period
  comparison, sources, geography, retention, active hours.
- New catalog components: Heatmap and RetentionGrid.
- DateTimePicker in the toolbar (quill): named ranges roll to now, Custom
  pins to absolute; view re-runs queries, edit feeds the agent via a
  [Range] prompt line.
- Multi-value query refresh: shape-tagged queries mapped onto props via a
  JSON-pointer setter; timezone-safe {date_from}/{date_to}(+_prev) tokens.
- Channels remember collapsed state in localStorage (closed by default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adamleithp adamleithp force-pushed the feat/canvas-web-analytics branch from 75f4d8f to b9a5559 Compare June 13, 2026 21:59
@adamleithp adamleithp enabled auto-merge (squash) June 13, 2026 21:59
@github-actions

Copy link
Copy Markdown

React Doctor could not complete this scan.

No React dependency found in /tmp/react-doctor-baseline-UWO2xU/package.json. Add "react" to dependencies (or peerDependencies) and re-run.

Reviewed by React Doctor for commit b9a5559.

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates denied this PR: it modifies dependencies/toolchain (pnpm-lock.yaml, package.json files, pnpm-workspace.yaml) and exceeds the size ceiling significantly (3783 lines, 48 files across two feature areas). This requires human review.

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jun 13, 2026
@adamleithp adamleithp added the Stamphog This will request an autostamp by stamphog on small changes label Jun 14, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates denied this PR: it modifies dependencies/toolchain files (pnpm-lock.yaml, multiple package.json files, pnpm-workspace.yaml) and far exceeds the size ceiling at 3783 lines across 48 files spanning two feature areas. Human review is required.

@stamphog stamphog Bot removed the Stamphog This will request an autostamp by stamphog on small changes label Jun 14, 2026
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.

1 participant