Skip to content

feat(questionnaires): F8.2 result exports#54

Merged
JohnD-EE merged 1 commit into
mainfrom
feat/F8.2-result-exports
Jun 8, 2026
Merged

feat(questionnaires): F8.2 result exports#54
JohnD-EE merged 1 commit into
mainfrom
feat/F8.2-result-exports

Conversation

@JohnD-EE

@JohnD-EE JohnD-EE commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds F8.2 — result exports, the record-level read-side companion to F8.1's aggregate analytics. An admin can download a version's completed-session results from the analytics page as:

  • CSV — one row per session × question (the lossy, spreadsheet-friendly view; every cell run through csvEscape).
  • JSON — the full session graph: answers + provenance + per-turn transcript (faithful, machine-readable; returned bare so the file is the data).

A single admin-only GET .../versions/[vid]/export?format=csv|json serves both, reusing the F8.1 analytics filter (date window + tags) so an export mirrors exactly what the admin is viewing.

Anonymous-mode contract

When the version's AppQuestionnaireConfig.anonymousMode = true, the loader (results-loader.ts) honours it at the data boundary, not just the UI:

  • respondent identity is nulled, and
  • every session's turns array is dropped (raw respondent prose never leaves the server — turns are still loaded so answer→turn-ordinal math works, but they're never serialised out).

Answer values are always present in both formats — anonymity is about non-linkage, not redacting the survey data (mirrors the F7.4 PDF export). Full cross-surface anonymous audit is deferred to F8.3.

Decisions (confirmed with the user)

  • Anonymous → null identity + drop turns (above).
  • Completed sessions only (non-preview, status completed, createdAt in window). Status is still a column/field.
  • One route, ?format=csv|json (default json), mirroring the orchestration conversation-export route.
  • Reuse, don't rebuild — F8.1 scope/query contract, csvEscape, the single-session PDF loader's anonymous stance, and the exportLimiter bulk-read sub-cap. No migration.

Changes

  • Export librarylib/app/questionnaire/export/{results-types,results-loader,results-serialize,results-query}.ts. loadResultsExport batches the single-session PDF loader; one batched user.findMany for names (no N+1); MAX_EXPORT_SESSIONS = 5000 cap → capped.
  • Routeapp/api/v1/app/questionnaires/[id]/versions/[vid]/export/route.ts (master-flag-gated, version-scoped, exportLimiter sub-cap) + API.APP.QUESTIONNAIRES.versionExport(id, vid).
  • UIcomponents/admin/questionnaires/analytics/export-buttons.tsx, wired into the analytics page beside the version selector, carrying the page's current filter.
  • Docs — Exports section in .context/admin/questionnaire-analytics.md; .context/app/planning/features/f8.2.md tracker.

Testing

  • Unit: serialiser (CSV row-per-session×question, formula-injection escaping, renderAnswerValue, JSON fidelity), loader (completed-only window, batched name resolution only when not anonymous, anonymous nulls identity + drops turns without querying users, turn-ordinal mapping, cap, tag filter, null/fallback branches), query schema, export-buttons component (URL build, 429/error, blob download).
  • Integration: route shell — flag-gate / auth / version-scope / bad-date 400 / default-JSON vs ?format=csv content-type + attachment filename + no-store / resolved scope reaches loader / exportLimiter 429.
  • Full suite: 22,008 passed. npm run validate clean. Changed files all ≥80% coverage.
  • No CHANGELOG entry — tracks only the Sunrise platform surface; ConQuest app routes are out of scope.

🤖 Generated with Claude Code

Add the record-level read-side companion to F8.1's aggregate analytics:
download a version's completed-session results as CSV (one row per
session × question) or JSON (the full session graph — answers +
provenance + turns). A single admin-only GET route serves both via
?format=, reusing the F8.1 analytics filter (date window + tags) so an
export mirrors exactly what the admin is viewing.

- Export library (pure serialisers, Prisma at the seam) in
  lib/app/questionnaire/export: results-types, results-loader
  (loadResultsExport — batched cousin of the single-session PDF loader;
  MAX_EXPORT_SESSIONS=5000 cap → capped), results-serialize (csvEscape +
  renderAnswerValue / bare JSON), results-query (analytics filter +
  format).
- Route .../versions/[vid]/export (master-flag-gated, version-scoped,
  exportLimiter bulk-read sub-cap) + versionExport endpoint builder.
- Analytics-page export buttons (export-buttons.tsx) carrying the page's
  current filter; blob download via Content-Disposition.

Anonymous mode (AppQuestionnaireConfig.anonymousMode) is honoured at the
data boundary: respondent identity is nulled AND every session's turns
array is dropped (raw respondent prose never leaves the server). Answer
values are always present — anonymity is non-linkage, not redaction.
Completed sessions only.

Tests: serialiser + loader unit (anonymous redaction, completed-only
window, batched name resolution, turn-ordinal mapping, cap, branches),
export-buttons component, route integration (auth/flag/scope/format
headers/429), versionExport endpoint assertion. Docs + F8.2 tracker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JohnD-EE JohnD-EE merged commit e83fdd3 into main Jun 8, 2026
14 checks passed
@JohnD-EE JohnD-EE deleted the feat/F8.2-result-exports branch June 8, 2026 18:59
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