# Insights (https://form.dimah.dev/docs/insights)



`@dimah-form/insights` is the first-party **summary** plugin for admin reporting. It walks stored responses (same 100-row pages as list, capped at 10\_000 by default) and folds **snapshot** answers into:

* totals by `draft` / `submitted` / `abandoned`
* visible-required completion among submitted rows (`completion.rate` is `complete / submitted`, or `null`)
* per-field stats: `n` is **visible** occurrences; `hidden` is skip-logic; `unanswered` is visible and empty
* value counts + `pct` (0–1) for categorical fields (`select` / `multiSelect` / `boolean`, or any field with `options`). Levels follow the document catalog, including unused `n: 0`, in document order — not by frequency
* `numeric` min / max / mean / stdev for any finite number answer (`number`, or a custom type such as `rating`); `dates` min / max for `date`
* scoring band / mean / stdev from `@dimah-form/scoring` (a direct dependency). Bands follow `meta.scoring` order, including unused `n: 0`
* optional `bucket=day` series of `submittedAt` (`timeZone`, default `UTC`)
* `GET /insights/crosstab` for two categorical fields. `n` is respondents who contributed a cell, not multiSelect tokens

Derived data only. No tables, no `meta` namespace, no live-form flatten. The live questionnaire only supplies field order and catalog levels that snapshots did not already list. Text / email fields are answered / unanswered only.

This is not a warehouse. For large N persist rows with [`datasetPlugin({ onProject })`](https://form.dimah.dev/docs/dataset) and aggregate in your database. When the walk hits the cap, `truncated` is true — lower `maxRows` on the query, or raise the plugin cap and project instead.

***

## Install [#install]

```bash
pnpm add @dimah-form/insights
```

```ts title="lib/form.ts"
import { insightsPlugin } from "@dimah-form/insights";
import { dimahForm } from "@dimah-form/server";

export const form = dimahForm({
  database,
  forms,
  plugins: [insightsPlugin()],
});
```

```ts title="lib/client.ts"
"use client";

import { insightsClientPlugin } from "@dimah-form/insights/client";
import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";

const plugins = [insightsClientPlugin()] as const;
export const formClient = createFormClient<Form, typeof plugins>({
  plugins,
});
```

Guard `getFormInsights` and `getFormCrosstab` like `listResponses` (admin).

There is no default `status` filter (unlike dataset, which defaults to submitted). Pass `status`, `respondentId`, `submittedFrom` / `submittedTo`, and `updatedAfter` to match `listResponses`. Segment in the fold with `whereField` + `whereValue` (unknown field → empty counts, not 404). A finite number matches the decimal string of the stored value (`5` matches `"5"`). `multiSelect` `%` is of **answered respondents** (can sum above 1). Categorical `%` and score bands include every catalog level, so a scale stays in document order when a level has `n: 0`.

```ts
const summary = await form.api.getFormInsights({
  query: { formId: "gad7", status: "submitted" },
});

const byDay = await form.api.getFormInsights({
  query: { formId: "gad7", status: "submitted", bucket: "day" },
});

const table = await form.api.getFormCrosstab({
  query: { formId: "gad7", row: "city", col: "remote", status: "submitted" },
});
```

`summary.byStatus` is the census. `completion` uses visible required fields on **each row’s snapshot**. `fields` follow the live questionnaire order; ids that exist only on older snapshots come after. `scores` is omitted when the snapshot has no `meta.scoring`, or when that snapshot’s scoring meta fails validation. A crosstab axis counts only on snapshots where that field is categorical. Changing the live field to text does not drop older select rows, and it does not reject the query. An id that is only on older snapshots still folds. `table.n` is the respondent base for the cells.

`bucket=day` groups `submittedAt` in an IANA `timeZone` (query overrides the plugin option; default `UTC`). The series includes that `timeZone`.

`GET /insights/summary` and `GET /insights/crosstab`. Filename, charts, and zip stay in the app. Optional UI belongs in the consumer — `@dimah-form/ui` is the fill renderer, not an admin dashboard.

***

## Out of this package [#out-of-this-package]

Weighted samples, LLM summaries, persisting the summary, and charts. Export files are [`@dimah-form/dataset`](https://form.dimah.dev/docs/dataset).
