Analytics & Reporting Suite
A guided tour of the four reporting surfaces in Metro2: the Data Quality dashboard, the branded PDF audit report, submission reconciliation against CRA responses, and the month-over-month trend dashboard.
1. Overview
The Analytics & Reporting Suite is four interconnected surfaces that share a single source of truth: the Metro2 validation rule registry. Whether you are previewing a file before submission, signing off a compliance audit, reconciling what the bureau actually accepted, or watching your data quality drift month over month, the rules and severities you see are the same ones used by the file generator.
Each surface is designed to be useful at a different moment in your Metro 2 lifecycle:
| Surface | When you use it | Output |
|---|---|---|
| Data Quality dashboard | Before you cut a file — preview rule pass/fail and a weighted DQ score. | Live KPI cards, failing-rule table, drill-down to offending records. |
| PDF Audit Report | After a submission — generate a co-branded compliance artifact. | Letter-size PDF stored in a private bucket; optional typed sign-off. |
| Submission Reconciliation | After the bureau returns a response file — what did they accept? | Sent / accepted / rejected funnel for a specific transmission, CSV + PDF export. |
| Trend dashboard | Monthly compliance reviews — is data quality improving or sliding? | Month-over-month DQ score line, severity mix, bureau rejection rate. |
The shared rule registry means a "critical" finding looks the same in every surface — same rule ID, same description, same severity, same weight. There is no parallel taxonomy to learn.
2. The Data Quality Score
Every validation run produces a single numeric score between 0.000 and 1.000. We call this the passing score. It is the headline metric on the DQ dashboard, the cover-page KPI on the PDF audit report, and the y-axis on the trend dashboard.
How it's computed
Each finding the validator emits is assigned a severity. Severities carry a weight; we sum the weights across the run and divide by the total number of rule evaluations performed. The score is then the complement of that ratio, clamped to [0, 1].
weighted_findings = (critical_count × 10)
+ (error_count × 5)
+ (warning_count × 1)
+ (info_count × 0)
passing_score = clamp01(1 − weighted_findings / total_evaluations)Where total_evaluations = total_records × ~30 rules. The "30 rules per record" denominator is an estimate of the average number of registry rules that apply to a single Metro 2 record under the standard preset — the DQ dashboard surfaces this denominator explicitly so you can sanity-check the math.
Severity weights
| Severity | Weight | What it means | Bureau impact |
|---|---|---|---|
| critical | 10 | Will almost certainly be rejected by the CRA on intake. | Hard reject — the bureau drops the record entirely. |
| error | 5 | Violates a documented CDIA rule; the field is not compliant. | Usually accepted with errors; may suppress display fields. |
| warning | 1 | Soft advisory — best practice rather than a hard rule. | Bureau accepts silently; you may see downstream display oddities. |
| info | 0 | Informational — surfaced for transparency, never penalized. | No impact. Score is unaffected. |
What does a "passing" run look like?
There is no single industry-standard threshold, but in practice we see these score ranges across the customer base:
| Range | Label | Typical interpretation |
|---|---|---|
| 0.995 – 1.000 | Excellent | Either zero critical findings and a handful of warnings, or a clean file. Safe to submit. |
| 0.970 – 0.994 | Healthy | A few errors but no criticals. Ship it; remediate in the next cycle. |
| 0.900 – 0.969 | Watch | Material errors present. Review the failing-rule table before you generate the file. |
| Below 0.900 | Hold | Criticals or systemic field errors — fix at the source mapping before submitting. |
3. DQ Dashboard
Route: /dashboard/analytics/quality
The DQ dashboard is the day-to-day operator view. It shows the latest validation run for your active company, plus the full history of runs you can scroll back through. Every numeric KPI links to the underlying records — you should rarely need to leave this page to investigate a failing rule.
KPI cards
The top of the page has six KPI cards that summarize the most recent run:
- Passing Score — the weighted score described above, formatted as a percentage with three decimals.
- Records Evaluated — how many
metro2_recordsrows the run touched. - Total Findings — sum across all severities. Clicking the card filters the findings table to the full set.
- Critical / Error / Warning — three separate cards for the three penalized severities. Clicking each filters the findings table to that severity.
A sample header for a healthy run might look like:
Passing Score: 99.62% (1.000 = perfect)
Records Evaluated: 14,802 (444,060 checks)
Total Findings: 168
Critical: 0
Errors: 12
Warnings: 156Failing-rule table
Below the KPIs is the failing-rule table. Each row is one rule from the registry that produced at least one finding in this run, sorted first by severity then by frequency. The columns are:
| Column | Description |
|---|---|
| Severity | Colored pill matching the weights table above. |
| Rule ID | Permanent registry ID, e.g. SEG_HDR_RECORD_LENGTH. Quote this in remediation tickets. |
| Description | Plain-English rule name, plus the CDIA reference if one is on file. |
| Findings | Number of individual occurrences across all records in this run. |
| Records | Number of distinct records that have at least one finding for this rule. |
| Action | Drill-down link — opens the affected records in a filtered list view. |
Drill-down to failing records
Clicking the drill-down chevron on any row opens the standard records view filtered to that rule ID and run ID. You can:
- Click any record to open its full editor with the failing field highlighted.
- Bulk-select records and apply a fix or a tag.
- Export the filtered set to CSV (preserves the rule ID column so you can hand it to a developer).
Kicking off a manual run
The dashboard auto-runs validation after every file import, but you can also start one on demand. Use the Run validation button at the top right. The dialog lets you choose:
- Scope —
company(all records),file(a specific import), ortransmission(only what is queued for the next submission). - Jurisdiction —
USorCA. Defaults to the per-record value stored on each row; override only if you are intentionally evaluating a CA file against US rules or vice versa. - Max records — soft cap (default 100,000) so you don't accidentally kick off a multi-hour job on a giant portfolio.
Runs are persisted to metro2_validation_runs with full findings in metro2_validation_findings — you never lose the audit trail by re-running.
4. PDF Audit Report
Route: /dashboard/analytics/reports
The PDF audit report is a polished, co-branded compliance artifact you can hand to an auditor, an underwriter, or your own internal compliance team. It bundles a run summary, the top failing rules, the bureau reconciliation funnel (if available), and a typed attestation block into a single Letter-size PDF.
Generating a report
- From the Reports page, click New audit report.
- Pick a validation run from the drop-down. The default is the most recent run, but you can attach a report to any historical run.
- Optionally attach a bureau transmission. If you do, the PDF includes the Reconciliation page (sent / accepted / rejected / top rejection codes). If you leave it blank, that page is omitted.
- Optionally provide a customer logo URL (see Setting your company logo).
- Click Generate. Rendering takes ~108 ms on a warm Lambda, plus a few hundred ms to upload the PDF to private storage. You will see the new report at the top of the list when it's ready.
What's in the PDF
- Cover page — Metro2 wordmark, your customer name, optional customer logo, validation-run ID, scope, and run date.
- Data Quality Summary — the six KPI cards from the dashboard, rendered as printable cards with severity coloring.
- Top Failing Rules — table of the 25 rules with the highest finding counts, each with its permanent rule ID and CDIA reference (when available).
- CRA Reconciliation — sent / accepted / accepted with warnings / updated / rejected counts plus the top rejection codes from the bureau response. Only included when a transmission is attached.
- Compliance Attestation — typed sign-off block. If the report has not yet been signed, this page shows "Awaiting sign-off." Once countersigned, regenerating the PDF stamps the attesting officer's name, title, and timestamp.
- Every page carries the Metro2 wordmark, the customer name, page numbers, and a "confidential" footer.
Co-branding
The PDF is always co-branded: the Metro2 wordmark appears on every page header. When you supply a company_logo_url in the generate request, that image is placed next to the wordmark on the header bar (sized 36×36 pt) so the document reads as a Metro2 compliance artifact prepared on behalf of your company.
PII redaction
Record-level rows in the PDF show only the last 4 digits of the SSN — never the full SSN, never the full account number. This is intentional and not configurable: the PDF is designed to be sharable with third-party auditors who should not see raw PII.
Past reports
The Reports page lists every PDF ever generated for your company, newest first. Each row shows:
- Run scope and date.
- Transmission (bureau + date) if one was attached.
- Sign-off status (Awaiting / Signed).
- Download button (returns a signed URL valid for 60 minutes).
- Sign-off button (if you haven't already attested).
Where are PDFs stored?
Generated PDFs live in a private Supabase Storage bucket named audit-reports. The bucket is not public; access is gated by your company's RLS policies. We hand out a signed URL each time you click Download, and those URLs expire after 60 minutes so you can't accidentally leak a long-lived link.
File naming convention:
audit-reports/
{company_id}/
{report_id}--{YYYY-MM-DD}.pdf5. Reconciliation Report
Route: /dashboard/analytics/reconciliation/[transmissionId]
Reconciliation answers a single question: "Of the records I sent the bureau, how many did they actually accept?" Every CRA returns a response file after they process your submission; we parse those responses and join them to the original records, then surface the diff as a funnel.
The funnel
Example funnel for a 14,802-record submission:
| Stage | Count | % of sent |
|---|---|---|
| Sent to bureau | 14,802 | 100.0% |
| Accepted (clean) | 14,640 | 98.9% |
| Accepted with warnings | 118 | 0.8% |
| Updated existing tradeline | 13,981 | 94.5% |
| Rejected | 44 | 0.3% |
"Updated existing" is a subset of "Accepted" — it's broken out separately because customers care a lot about whether they're creating new tradelines or refreshing ones the bureau already has.
Top rejection codes
Below the funnel is a "Top Rejection Codes" table — the most common reasons the bureau pushed records back. Each row has the bureau's rejection code, a sample message from one of the rejected records, and the count. Hover or click a code to expand the full list of affected records.
Code Sample message Count
-------- ------------------------------------------------------------ -----
R022 Invalid date of last activity (future-dated) 18
R107 SSN does not match prior submission for this account 12
R301 Account number reused across portfolios 9
R411 Payment history profile contains invalid character "X" 5CSV export and PDF
- Export CSV — downloads the full record-level join: every sent record with its bureau status, rejection code (if any), and rejection message. Useful for piping into a remediation ticket pipeline.
- Generate PDF — wraps the reconciliation funnel into an Audit Report PDF for this specific transmission. This is the same PDF described in section 4, but with the transmission pre-attached.
6. Trend Dashboard
Route: /dashboard/analytics/trends
The trend dashboard turns months of validation runs and bureau responses into a long-view picture of your program's compliance trajectory. Three charts on one page:
The three charts
- DQ score over time — a line chart of the monthly average passing score for the last 12 months. Hovering shows the month's run count and total findings.
- Severity mix — a stacked bar (critical / error / warning / info) for each month. This is where you'll spot a sudden spike in criticals even if the score is still healthy.
- Bureau rejection rate — line chart of rejected / sent per month, optionally split per bureau (Equifax, Experian, TransUnion, Innovis).
Per-portfolio vs company-wide
A toggle at the top of the page switches between:
- Company-wide — every record, every portfolio, aggregated. This is the default and the right view for monthly executive reviews.
- Per-portfolio — a small-multiples grid with one mini-chart per portfolio. Use this when you furnish multiple programs (e.g. auto loans + BNPL + revolving) from one company account and want to know which line of business is dragging the score down. See Multi-Portfolio Routing for how portfolios are set up.
Exporting the trend
Both charts have an Export dropdown:
- CSV — one row per month per portfolio (or per company in company-wide mode), with all three metrics. Useful for BI tools or board-deck spreadsheets.
- PNG — chart snapshot for slides.
- PDF — a "Trend Summary" PDF that wraps all three charts and a short narrative.
month dq_score runs critical error warning sent rejected reject_rate
2026-01 0.9912 11 0 47 612 142,118 411 0.29%
2026-02 0.9941 11 0 22 503 143,902 298 0.21%
2026-03 0.9876 12 3 71 809 147,560 480 0.33%
2026-04 0.9950 12 0 18 466 148,201 265 0.18%7. Where the data comes from
Validation rule registry
Every finding in every dashboard traces back to a row in metro2_validation_rules. We ship 56 system rules covering every required Metro 2 segment (Header, Base, J1, J2, K1–K4, L1, N1, Trailer) plus the most common CDIA-defined consistency rules. Each rule has:
- A permanent rule ID — e.g.
BASE_DATE_OPENED_FUTURE. The ID is stable across releases, so if you write a remediation playbook against a rule it won't break when we ship new rules. - A severity — assigned by us based on bureau documentation and observed rejection behavior. Severity drives the score weights.
- A CDIA reference — the section number in the CDIA Credit Reporting Resource Guide, where applicable.
- A category — Field Format, Cross-field Consistency, Date Logic, Segment Required, etc. Used for grouping in the failing-rule table.
Adding a new rule (or re-leveling an existing rule's severity) is a release event — the rule registry is versioned alongside the application. Customer-specific rule overrides are on the roadmap but not yet available.
Materialized view refresh
Trend-dashboard performance is backed by three materialized views:
| Matview | Granularity | What it stores |
|---|---|---|
metro2_dq_monthly | month × company | Average DQ score, total findings, severity counts. |
metro2_run_monthly | month × company × scope | Run counts, records evaluated, total evaluations. |
metro2_transmission_monthly | month × bureau × company | Sent, accepted, rejected, top rejection codes. |
These are refreshed nightly at 02:00 UTC by a Vercel cron job (/api/cron/refresh-dq-monthly). That means the trend dashboard always lags real-time by up to 24 hours — if you import a file at 10:00 UTC today, you'll see it in the company-wide trend tomorrow morning. The DQ dashboard and PDF report are live and do not depend on the matviews.
CRA response files
Accepted-vs-rejected numbers come from the bureau response files we ingest after each transmission. Equifax, Experian, TransUnion, and Innovis each have slightly different response formats; we normalize them into a common shape on import and join them to your sent records by account number + SSN hash. See the CRA Responses guide for the ingestion details and how to manually re-process a response file.
8. Setting your company logo
Today, the company logo for the PDF audit report is passed per-request in the body of the generate call:
POST /api/analytics/reports/audit-pdf
{
"run_id": "9b25...c4",
"transmission_id": "b71f...92",
"company_logo_url": "https://cdn.example.com/acme-logo.png"
}The "New audit report" dialog also exposes a logo URL field that maps to this same parameter. If you omit it, the cover page renders without a customer logo — just the Metro2 wordmark.
- Use a square image at least 144×144 px. We render the logo at 36×36 pt (~48 px at 96 DPI) on the page header and 96×96 pt on the cover hero.
- PNG with transparency works best. JPEG is fine. SVG is not supported by @react-pdf/renderer.
- The URL must be publicly reachable from our render workers — no private CDN, no signed URLs, no localhost. If the fetch fails, the PDF still renders but the logo slot is empty (we log a warning).
Permanent upload (roadmap)
A permanent "company logo" upload field on the company settings page — which would default into the generate request — is on the roadmap but not yet shipped. Until then, the recommended pattern is to host your logo at a stable URL (your marketing site CDN works fine) and either paste it into the dialog or have your integration include it in the API call.
9. Typed attestation
The last page of every audit-report PDF is a Compliance Attestation block. It exists so a designated officer at your company can put their name to the run — confirming that the data quality findings accurately represent the file at the time of generation.
What "typed" attestation means
We use a typed sign-off rather than electronic-signature (DocuSign, Adobe Sign, etc.) on purpose. The FCRA does not mandate an electronically-signed compliance artifact for furnisher data quality reports, and most of our customers' internal compliance programs are satisfied with a named, dated, role-stamped attestation. The PDF page records:
- Name — the typed full name of the attesting officer.
- Title — e.g. "VP, Credit Operations" or "Compliance Manager".
- Timestamp — the UTC moment the sign-off was submitted via the console.
The block on the PDF itself contains the disclaimer text: "This is a typed attestation per the customer's compliance program; FCRA does not mandate electronic signature for furnisher data quality reports."
Who can sign
Any user with the compliance_signer role on your company can attest. By default, company owners and admins have this role; you can grant it to other users from Settings → Team. The sign-off action is recorded in the audit log with the signer's user ID, the run ID, the report ID, and the IP address of the request.
Audit log retention
Sign-off events live in the company audit log forever — they are never purged. The PDF files themselves are also retained indefinitely in the audit-reports bucket. If you need to re-render a signed report (for example, because you updated the company logo and want the new branding on a historical artifact), use the Regenerate action on the report row; the new PDF will carry the same attestation block but the refreshed cover.
10. Common questions
/api/cron/refresh-dq-monthly). The current day's runs are not yet in the trend — they show up the next morning. If you need a same-day view, use the DQ dashboard, which reads live from metro2_validation_runs.archived_at column), but the row stays in the database forever and the underlying findings table is never purged. PDFs in the audit-reports bucket can be redacted if they contain something genuinely sensitive — contact support.900 / 30,000 = 0.03, so the score comes out around 0.97. If the file is even smaller, the score drops further.11. API reference
Every dashboard surface is backed by a REST endpoint under /api/analytics/*. All endpoints accept either a session cookie (when called from the console) or a bearer API key (Authorization: Bearer YOUR_KEY). All responses are JSON. All routes are scoped to your company by the auth context — you cannot read another company's runs.
DQ runs
| Method + Path | Purpose | Notable params / fields |
|---|---|---|
GET /api/analytics/dq-runs | List recent validation runs for your company. | scope, limit, cursor |
POST /api/analytics/dq-runs | Kick off a new validation run. | Body: scope, file_upload_id, metro2_jurisdiction, max_records |
GET /api/analytics/dq-runs/{id} | Fetch a single run's summary (score, counts, scope, ran_at). | — |
GET /api/analytics/dq-runs/{id}/findings | Stream findings for a run, paginated and filterable. | severity, rule_id, limit, cursor |
Sample POST response:
{
"run": {
"id": "9b25e418-...-c4",
"company_id": "f01a...",
"scope": "company",
"ran_at": "2026-05-17T19:42:11Z",
"total_records": 14802,
"total_evaluations": 444060,
"passing_score": 0.9962,
"critical_count": 0,
"error_count": 12,
"warning_count": 156,
"info_count": 0
}
}Reconciliation
| Method + Path | Purpose |
|---|---|
GET /api/analytics/reconciliation/{transmission_id} | Returns the sent/accepted/rejected funnel + top rejection codes for one transmission. |
{
"transmission": {
"id": "b71f...92",
"bureau": "equifax",
"transmission_date": "2026-05-01",
"records_count": 14802,
"status": "responded"
},
"reconciliation": {
"sent": 14802,
"accepted": 14640,
"warning": 118,
"updated": 13981,
"rejected": 44,
"topRejectionCodes": [
{ "code": "R022", "count": 18, "sampleMessage": "Invalid date..." },
{ "code": "R107", "count": 12, "sampleMessage": "SSN does not match..." }
]
}
}Audit PDF
| Method + Path | Purpose |
|---|---|
POST /api/analytics/reports/audit-pdf | Generate a new PDF audit report. Synchronous, returns the report row. |
GET /api/analytics/reports/audit-pdf | List past reports for your company. |
POST /api/analytics/reports/audit-pdf/{id}/sign-off | Record a typed attestation against a report. |
Sample generate request:
curl -X POST https://metro2.switchlabs.dev/api/analytics/reports/audit-pdf \
-H "Authorization: Bearer $METRO2_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"run_id": "9b25e418-...-c4",
"transmission_id": "b71f...92",
"company_logo_url": "https://cdn.example.com/acme-logo.png"
}'Sample sign-off request:
curl -X POST \
https://metro2.switchlabs.dev/api/analytics/reports/audit-pdf/abc-123/sign-off \
-H "Authorization: Bearer $METRO2_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signed_off_by_name": "Jane Smith",
"signed_off_by_title": "VP, Credit Operations"
}'Trend
| Method + Path | Purpose |
|---|---|
GET /api/analytics/trend | Returns month-over-month rows for the trend dashboard. |
Supported query params:
months— how many months back to return (default 12, max 36).portfolio_id— restrict to a single portfolio. Omit for the company-wide view.bureau— restrict the rejection-rate series to one CRA (equifax,experian,transunion,innovis).format—json(default) orcsv.
GET /api/analytics/trend?months=6&format=json
{
"trend": [
{
"month": "2026-01",
"dq_score": 0.9912,
"runs": 11,
"critical": 0, "error": 47, "warning": 612, "info": 9,
"sent": 142118, "rejected": 411
},
...
]
}12. Troubleshooting
The trend dashboard shows stale data
The most likely cause is that the nightly cron didn't run, or ran but failed.
- Check the dashboard's "Data freshness" badge in the top-right corner. If it reads more than 26 hours old, the cron has missed at least one cycle.
- Look at the most recent run for the cron endpoint
/api/cron/refresh-dq-monthlyin Vercel logs. A non-200 response is the smoking gun. - As a workaround, an admin can hit the cron endpoint manually:
curl -X POST https://metro2.switchlabs.dev/api/cron/refresh-dq-monthly -H "Authorization: Bearer $CRON_SECRET". The endpoint is idempotent. - If the cron is running but the data is still stale, the matviews may be locked by a long-running query. Contact support.
PDF render fails
PDF rendering is fast (~108 ms warm) but it can fail for two main reasons:
- Unreachable logo URL. If
company_logo_url404s, times out, or returns a non-image content type, the PDF still renders but the logo slot is empty and a warning is logged. If you supplied a URL and don't see the logo, check it from a browser first. - Run has no findings AND no records. An empty run will render a degenerate PDF (cover + "No findings recorded" placeholder). If you see a request 500 with
run_not_found, the run ID you passed doesn't belong to your company. - Cold-start timeout. Rare, but possible on serverless cold starts. Retry once. If it persists, contact support with the request ID.
Reconciliation shows fewer accepted than sent
This is usually a CRA-response-file processing lag rather than an actual problem with the submission. The bureaus return their response files on their own schedule (typically 24–72 hours after they process the submission), and we ingest them as they arrive.
- Look at the transmission's
statusfield. If it'ssentrather thanresponded, the bureau hasn't returned a response file yet — the funnel is reflecting a partial picture. - If the status is
respondedbut the "accepted" number is lower than expected, the response file may have only acknowledged a subset of the submission. Open the CRA Responses guide for the per-bureau quirks (Experian, for example, sometimes sends two response files: one for accepted records and a follow-up for rejected). - As a last resort, re-ingest the response file from the CRA Responses page; it's idempotent and will reconcile what we have on disk.
DQ run shows 0 records evaluated
Almost always one of:
- You ran the validation against a file upload ID that has no records (the file failed to import). Check
/dashboard/uploadsfor the file's status. - You selected scope
transmissionand there's nothing queued for the next transmission yet. Selectcompanyinstead. - The company truly has zero records imported. The dashboard's empty state should make this obvious — if it doesn't, you may have a company-context mismatch (check the company switcher in the top nav).
Analytics API returns 403 with a valid key
Your API key is scoped to a single company, but the resource you're asking for (a run ID, a transmission ID, a report ID) belongs to a different company. The 403 is intentional — keys cannot cross company boundaries even if your user account has access to both. Generate a separate key for the other company under Settings → API Keys.
13. Related guides
If your question isn't answered here, reach out via the contact form — include the run ID, transmission ID, or report ID and we can usually root-cause within a few minutes.
Still stuck? Contact support or browse the Help Center.