Skip to content

Name translation

Source trackers put no meaningful restriction on metric names — they are dict keys, so any string goes. Metrana validates names on a denylist: no whitespace or control characters, none of the query-language reserved set ( ) [ ] , = < > ! ~ " |, no leading :, and byte caps.

Migration bridges the two by substituting every rejected character. Nothing is deleted, so a migrated name still reads like the original.

Source Becomes
space, tab, newline, control characters _ runs collapse to one
( [ {
) ] }
, ;
= :
< .lt.
> .gt.
! .not.
~ .approx.
" '
| -
leading : _ first character only; : is legal elsewhere

In practice:

Source name Migrated name
train/loss train/loss — unchanged, / is legal and carries the hierarchy
accuracy (%) accuracy_{%}
grad_norm[0] grad_norm{0}
precision,recall precision;recall
lr=3e-4 lr:3e-4
acc>0.9 acc.gt.0.9
GPU Utilization GPU_Utilization

Unicode is untouched — état/loss migrates as-is.

Names longer than the 256-byte metric-name cap are truncated on a character boundary, never mid-character.

Substitution is many-to-one, so two different source keys can land on the same name. loss(train) and loss[train] both sanitize to loss{train} — and silently merging two series into one is worse than an ugly name.

When a name is claimed by more than one key, every claimant gets a three-letter slug:

'loss(train)' -> 'loss{train}-wax'
'loss[train]' -> 'loss{train}-yuu'

Including the one that would otherwise have kept the bare name. Two properties make this safe for a resumable migration, which rebuilds the mapping from scratch on every pass:

  • Slugs are derived from the source key, not handed out by a counter. A counter would produce different slugs if a later pass enumerated keys in a different order, splitting one series into two.
  • All claimants are slugged, so “who got there first” never leaks into the naming.

The result is identical no matter what order the keys arrive in, and identical across runs of the command. Every collision is named in the report, with all of its claimants.

Run names are also portal URL path segments, so they replace / \ ? # % on top of the map above — / and \ become -, the rest become _. Then the source prefix is applied (wandb-), and a run id suffix if the name is contested. See Weights & Biases.

Config keys become attribute paths, which are /-delimited and built by flattening nested dicts. A / inside a single config key would fabricate a level that was never there, so it becomes .:

{"learning/rate": 0.1} -> config/learning.rate

Other rejected characters follow the same map as metric names, against the larger 512-byte cap.

Unlike a metric name, a colliding attribute path is not slugged apart — the path is how the attribute is read back, so renaming it would hide the value under a name nobody looks for. Two keys that sanitize onto one path keep the first and drop the rest, and every dropped key is named in the report:

attribute collision on 'config/learning.rate': kept 'learning/rate', dropped 'learning.rate'

--dry-run prints the complete mapping — every rename and every collision — without writing anything:

Terminal window
metrana migrate wandb --entity my-team --project sweeps --workspace my-workspace --dry-run