Most production test logging is designed for the moment the test runs. An operator needs a green light or a red light, so the test prints one, and that is where the thinking stops.
The problem arrives months later. A customer returns a unit. A yield investigation needs a baseline. An auditor asks what a specific serial number measured. At that point the only thing anyone wants is the data nobody recorded.
This guide lists what to log, in rough priority order, and marks the fields that are painful to retrofit.
The minimum that makes data useful
The measured value, not just pass or fail.
This is the single highest-value field and the one most often missed. A boolean tells you a unit failed. The value tells you it measured 3.19V when the limit was 3.20V, and that it had been trending down for three weeks.
Everything downstream depends on this. Trend charts, capability analysis, drift detection and limit tuning are all impossible from booleans. If you record one thing beyond the verdict, record the number.
The limits that were applied.
Store the limits alongside the value, not only in the test code. Limits change, and when they do, a result recorded under the old limits becomes uninterpretable unless the limits travelled with it.
{ "name": "rail_3v3", "value": 3.31, "unit": "V", "lower_limit": 3.2, "upper_limit": 3.4, "passed": true}The units.
Cheap to record, and it prevents the class of error where someone later assumes millivolts.
A timestamp, in UTC.
Store UTC and convert for display. Two sites in two timezones will otherwise disagree about which day a failure occurred, and shift-boundary analysis becomes unreliable.
Identity: what was tested
Serial number. Without it you have statistics but no traceability, and you cannot answer any question about a specific returned unit.
Part number and revision. A measurement means something different on rev C than on rev A. Without the revision, mixed-revision data silently blends into one distribution.
Sub-assembly links. Which PCBA went into which enclosure, which module into which final assembly. This is the field most often skipped and most painful to add later, because retrofitting it means reconstructing relationships that were never captured.
Model it from day one even if you do not use it yet. When a component lot turns out to be bad, this is what tells you which shipped units contain it.
Context: where and how it was tested
These cost nothing at test time and are the dimensions you will want to split by during an investigation.
| Field | What it lets you answer |
|---|---|
| Station identifier | Is one station failing more than the others? |
| Operator | Is a fixture seating technique causing failures? |
| Test procedure version | Which version of the limits produced this result? |
| Firmware version on the DUT | Did a calibration constant change in software? |
| Fixture identifier | Is a specific fixture wearing out? |
| Instrument used | If a DMM was out of calibration, which units did it touch? |
The station identifier earns its place fastest. A single query grouping failures by station resolves a surprising share of yield investigations, and it is impossible without this field.
Test procedure version is the other one worth insisting on. Limits change over a product's life. If you cannot tell which version produced a result, your historical data stops being comparable at every limit change.
Execution detail
Per-step outcomes and durations. A run that failed tells you less than a run that failed at step 7 after passing 1 through 6. Step-level data is what makes failure Pareto analysis possible, and durations reveal a station slowing down before anyone notices.
Attachments where they matter. A waveform capture, a photograph of a failed unit, an instrument screenshot. Do not attach these on every run at volume, but attach them on failures, where they save a rework cycle.
Logs from the test script. Console output, instrument responses, exception traces. Cheap to store on failures, and the difference between diagnosing something and guessing.
What not to log
Restraint matters, because storage that grows without limit gets deleted wholesale, usually right before you need it.
Raw waveforms on every unit. Store the extracted features, rise time, overshoot, ripple, and keep the raw capture only for failures or a sample.
High-rate streams. If you are acquiring at hundreds of samples per second, aggregate before storing. A test record platform is for results, not for time-series acquisition. Record the mean, the extremes and the pass criterion, keep the full trace locally when you need it.
Anything you can derive. Do not store yield as a field. Compute it from runs. A stored aggregate is wrong the moment a retest arrives.
The four that hurt to retrofit
Everything above can be added later with some effort, except these:
- The measured value. Historical booleans cannot be turned back into numbers. Every day without this is a day of data you will never recover.
- Sub-assembly relationships. Reconstructing which board went into which assembly after the fact usually means reading build records by hand, if they exist.
- Procedure versioning. Once limits have changed without a version marker, you cannot reliably tell which results are comparable.
- Serial numbers on early units. Prototype and pilot units often skip serialisation, and those are exactly the units that come back with the most interesting failures.
A minimal schema
If you are storing to your own database, this covers the fields above:
CREATE TABLE unit ( id BIGSERIAL PRIMARY KEY, serial_number TEXT NOT NULL UNIQUE, part_number TEXT NOT NULL, revision TEXT, parent_id BIGINT REFERENCES unit(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now());CREATE TABLE run ( id BIGSERIAL PRIMARY KEY, unit_id BIGINT NOT NULL REFERENCES unit(id), procedure_id TEXT NOT NULL, procedure_version TEXT NOT NULL, station_id TEXT NOT NULL, operator TEXT, dut_firmware TEXT, started_at TIMESTAMPTZ NOT NULL, duration_s NUMERIC NOT NULL, passed BOOLEAN NOT NULL);CREATE TABLE step ( id BIGSERIAL PRIMARY KEY, run_id BIGINT NOT NULL REFERENCES run(id) ON DELETE CASCADE, name TEXT NOT NULL, duration_s NUMERIC, passed BOOLEAN NOT NULL);CREATE TABLE measurement ( id BIGSERIAL PRIMARY KEY, step_id BIGINT NOT NULL REFERENCES step(id) ON DELETE CASCADE, name TEXT NOT NULL, value DOUBLE PRECISION, unit TEXT, lower_limit DOUBLE PRECISION, upper_limit DOUBLE PRECISION, passed BOOLEAN NOT NULL);CREATE INDEX ON run (procedure_id, started_at DESC);CREATE INDEX ON run (station_id, started_at DESC);CREATE INDEX ON measurement (name, step_id);The parent_id on unit is the sub-assembly link. It is one column, and adding it now is far cheaper than reconstructing the relationships later.
Doing it without writing the schema
If your test scripts are Python, this shape comes for free. The measurement definition in your test code already carries the name, value, units and limits, so an output callback records every field above without a schema of your own.
With OpenHTF, that is one import and one line:
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("rail_3v3") .in_range(3.2, 3.4) .with_units(units.VOLT),)def power_rail_test(test): test.measurements.rail_3v3 = 3.31def main(): test = htf.Test( power_rail_test, procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", # procedure UUID from the dashboard part_number="PCBA-200", ) test.add_output_callbacks(upload()) test.execute(lambda: "SN-0001")if __name__ == "__main__": main()Install it as pip install "tofupilot[openhtf]". The upload() callback reads TOFUPILOT_API_KEY from the environment, and add_output_callbacks is OpenHTF's own extension point, so it composes with any callbacks you already use.
The point is not which tool you use. It is that the decision about what to record is made once, early, and is expensive to revisit. Record the value, version the procedure, model sub-assemblies, and keep UTC timestamps. The rest can wait.