Insights
Read-side counts from response snapshots — status, completion, field stats, score bands, optional day series and crosstab.
@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.rateiscomplete / submitted, ornull) - per-field stats:
nis visible occurrences;hiddenis skip-logic;unansweredis visible and empty - value counts +
pct(0–1) for categorical fields (select/multiSelect/boolean, or any field withoptions). Levels follow the document catalog, including unusedn: 0, in document order — not by frequency numericmin / max / mean / stdev for any finite number answer (number, or a custom type such asrating);datesmin / max fordate- scoring band / mean / stdev from
@dimah-form/scoring(a direct dependency). Bands followmeta.scoringorder, including unusedn: 0 - optional
bucket=dayseries ofsubmittedAt(timeZone, defaultUTC) GET /insights/crosstabfor two categorical fields.nis 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 }) 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
pnpm add @dimah-form/insightsimport { insightsPlugin } from "@dimah-form/insights";
import { dimahForm } from "@dimah-form/server";
export const form = dimahForm({
database,
forms,
plugins: [insightsPlugin()],
});"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.
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
Weighted samples, LLM summaries, persisting the summary, and charts. Export files are @dimah-form/dataset.