Most. Help

Legacy Tracker Data Migration: What Survives the Export

Published Oct 9, 202614 min readAdvanced
An old drawer feeds a conveyor through a sieve, and only the cards that keep their key reach the new feed
What you'll learn
  • The three inventories a legacy tracker export is audited against: keys, time bases, missing links
  • Why a conversion row can carry a receipt date while your report reads it as a visit date
  • Which rows the source had already unlinked before you decided to migrate anything
  • A repeatable side-by-side feed comparison keyed on your own columns, bucket by bucket
  • A written cutover criterion and the four conclusions it still cannot support
Advanced

Legacy tracker data migration starts with the fields in the export being inspected. Identify which fields associate conversions with clicks and which links remain observable in those rows. A missing value needs another source before it can be treated as recovered. Keep three inventories: association keys, the meaning of each timestamp, and records with incomplete links.

Switching delivery over is a different problem, and the corpus already covers it: Tracker Migration Without Losing Conversions handles in-flight clicks and postback continuity going forward. Legacy tracker data migration asks the narrower and older question: what leaves the old system as evidence, and when that evidence is good enough to declare a cutover.

Legacy Tracker Data Migration Audits Rows, Not Campaigns

A campaign-level claim like "our history is complete" has no observable referent. Legacy tracker data migration is an inventory exercise over those fields, and each of them lands in one of three classes: carried (present in the export under a documented name), derivable (reconstructible from other kept columns, with the derivation written down), or lost (never stored, or stored where the export cannot reach).

Item in a conversion rowWhere the key livesWhat a documented export carriesClass
Click identifierthe tracker's own click id, delivered as the postback parameter subida value plus the parameter name it arrived undercarried
Click identifier sent under another namealias mapping in postback settingsthe alias table, or nothing about its meaningderivable, if the config is exported too
Transaction identifieroptional tid, used to keep repeated conversions apartone row per unique tidcarried
Payout and its currencypostback parameters, sometimes absentamount, possibly without a currencyderivable or lost
Visit context of the conversionthe click record the postback matchedonly when a click key resolved at alloften lost

Keitaro's documentation states the join requirement plainly: "Without subid and status , the tracker cannot correctly process the postback." (Postback - Keitaro documentation) That single sentence defines the whole key inventory: a conversion row is only as migratable as the click key it was matched on, and the same page warns that a postback can also overwrite click-side values, since parameters such as cost, external_id and creative_id "will overwrite data previously recorded with the click from the traffic source".

Configuration travels or the keys lose their meaning. An alias that mapped clickid to the click key is not data; it is a rule, and a bare numeric column without its rule is an unexplainable value in five years. The click id is what a legacy row uses to claim it belonged to a visit at all, so legacy tracker data migration is measured by how many rows still hold one, not by how many rows the file contains.

Which Time Base a Legacy Row Actually Carries

Timestamps are the field most often migrated wrongly, because the column name promises more than the stored value delivers. A legacy conversion row can record: when the click happened, when the outcome happened at the advertiser, when the tracker received the postback, and when a report aggregated it.

The mismatch is documented, not hypothetical:

In the tracker, conversions are written by click date, but the Conversion Log is built not by click date, but by conversion date: this is the date of receiving the postback in the tracker.

- Keitaro documentation, Conversion log, docs.keitaro.io

Read together with the export contract - "Conversions are recorded by the date of the first postback received on subid for each unique tid value" - this means a CSV pulled from the conversion log is a receipt-time ledger wearing a conversion label, and legacy tracker data migration inherits that label unless the column is renamed before the first report is rebuilt. Two time bases per row, so the transferability inventory records which one a column holds before any report is rebuilt from it (cross-platform click id reference covers how long each platform's own click key stays resolvable).

Time baseWritten byPresent in a legacy exportLabel it as
Click timethe tracker at redirect timeyes, if the click record still existsvisit_time
Outcome time at the advertiserthe network or your own siteonly if it arrived as a parameteradvertiser_time, often absent
Postback receipt timethe tracker when it accepted the requestthis is what a conversion log sorts byreceipt_time
Report aggregation timewhatever dashboard you keptonly inside that dashboard's own tablesnot a row field
Visit time of an unlinked conversionnothingreplaced by receipt time in fallback pathsdegraded, must be marked

Export access and deletion settings limit different parts of the audit. Voluum Traffic Log can retrieve data up to four months old, but each selected report interval is at most seven days. These are lookback and request-range limits, not a storage-retention statement (Traffic log - Voluum documentation). In Keitaro, Stats store term controls automatic statistics cleaning; deleted statistics cannot be restored (Cleaning statistics - Keitaro documentation). Keitaro's Archive separately retains deleted resources such as campaigns and flows for a configurable period, with a default of 60 days. After that, those archived resources cannot be recovered (Archive - Keitaro documentation).

Record each limit separately before decommissioning the source: which dates the export can reach, how large each request may be, when statistics are purged, and which resources remain recoverable from an archive.

Voluum documents a fallback for conversions without a click ID. It attributes the conversion to a campaign rather than a visit, uses the postback timestamp as visit time, and treats each postback as unique without duplicate filtering. Currency defaults to USD unless supplied. The default fallback lacks parts of the visit context, but additional offer, lander, path or flow IDs can associate the conversion with selected funnel elements; custom variables must also be supplied explicitly. Those additions do not reconstruct a full visit (Registering Conversions Without Click IDs - Voluum documentation).

A request that never became a row leaves a different trace. Keitaro logs postbacks it discards: "If the status parameter is missing, the tracker ignores the postback and logs the event." That ignored-request log is evidence about the reporting gap between what a partner sent and what the source recorded, and it sits outside the conversion export entirely (postback troubleshooting checklist works through that boundary during live operation, not during a migration).

Counting them is the part of legacy tracker data migration that cannot be outsourced to an importer, and the inventory has three buckets rather than estimates:

  1. Unkeyed rows - conversions held without a click key, usually attributed to a campaign or an offer only. Preserve their available campaign or funnel context without presenting it as a reconstructed visit.
  2. Purged-key rows - a key existed and the click side is gone: retention passed, the server decommissioned, or a dump taken without the click table. The join is gone even though the amount is not.
  3. Never-recorded outcomes - requests the source ignored or never received. Nothing in the export represents them; only the discard log and the partner's own statement do.

Duplicate handling belongs to the same audit, because tid is what separates a repeated conversion from a re-sent one: the conversion log shows "conversions for one subid with different tid", while a repeated postback with identical parameters does not produce a second S2S send. Duplicate conversions across tracker, network and platform owns that mechanics; here it is only a column to classify.

Side-by-Side Event Feed: A Reproducible Comparison

A side-by-side feed is not a second dashboard. It is a claim you can re-run: the same interval, the same join key, the same time base, and every difference sorted into a named bucket. The column contract comes first, because comparing rows on columns that do not mean the same thing produces a number nobody can defend.

csv
click_key,tid,time_base,time_value,offer_id,lander_id,payout,currency,link_class

time_base is not decoration: it records whether time_value is click time or receipt time, and a legacy tracker data migration that drops it silently merges two ledgers disagreeing by design. link_class holds the bucket from the previous section, so an unlinked row stays visibly unlinked in the new feed instead of inheriting a plausible visit.

click_key alone is not a join key. The conversion log is documented to show "conversions for one subid with different tid", so repeated conversions of one click differ precisely in tid, and a comparison keyed on click_key would collapse them into a single line and report them as agreement. The pair click_key + tid is what the comparison below builds, and it is what the ladder of observations further down calls an accepted row. The pair is glued with a literal #, which is a valid composite key only while neither column contains that character; the guard below therefore refuses such a row instead of letting one string stand for two different pairs.

bash
# save as compare_feeds.sh and run with bash; one closed interval on both sides
set -euo pipefail
TB=receipt_time                       # the time_base value this comparison agrees on
printf 'click_key,tid,time_base,time_value,link_class\nsubid-1,t-1,receipt_time,2026-08-03T11:20:05Z,carried\nsubid-2,t-1,receipt_time,2026-08-04T09:10:00Z,carried\nsubid-3,t-2,receipt_time,2026-08-05T18:42:11Z,purged_key\nsubid-4,,receipt_time,2026-08-05T19:00:00Z,unkeyed\n' > old_export.csv
printf 'click_key,tid,time_base,time_value,link_class\nsubid-1,t-1,receipt_time,2026-08-03T11:20:05Z,carried\nsubid-2,t-1,receipt_time,2026-08-04T09:15:00Z,carried\nsubid-9,t-7,receipt_time,2026-08-06T07:00:00Z,carried\n' > new_feed.csv
: > excluded_rows.csv                 # rows the guard refuses to key stay visible here
keyed() {                             # one ledger -> "click_key#tid,time_value" rows
  tail -n +2 "$1" | awk -F, -v tb="$TB" -v bad=excluded_rows.csv '
    BEGIN { OFS = "," }
    $3 == tb && $1 != "" && $2 != "" && $1 !~ /#/ && $2 !~ /#/ { print $1 "#" $2, $4; next }
    { print >> bad }'
}
join -t, -a1 -a2 -e MISSING -o 1.1,2.1,1.2,2.2 \
  <(keyed old_export.csv | LC_ALL=C sort -t, -k1,1) \
  <(keyed new_feed.csv  | LC_ALL=C sort -t, -k1,1) > joined_rows.csv
awk -F, '{ if ($1 == $2 && $3 == $4) same++
           else if ($2 == "MISSING") only_old++
           else if ($1 == "MISSING") only_new++
           else time_diff++ }
     END { printf "joined_rows %d\nsame_key_same_time %d\nonly_in_old_export %d\nonly_in_new_feed %d\nkey_matched_time_differs %d\n",
           NR, same+0, only_old+0, only_new+0, time_diff+0 }' joined_rows.csv > bucket_counts.txt
printf 'excluded_unkeyable_rows %d\n' "$(awk 'END { print NR+0 }' excluded_rows.csv)" >> bucket_counts.txt
cat bucket_counts.txt

The example removes the header before sorting and keeps rows rejected by the key guard in excluded_rows.csv. Review that file alongside the matched rows. The script is a limited comparison of the shown fixture; adapt its parsing and key rules to your actual export before using it for a migration.

Expected output for the built-in fixture, six lines:

joined_rows 4
same_key_same_time 1
only_in_old_export 1
only_in_new_feed 1
key_matched_time_differs 1
excluded_unkeyable_rows 1

The fixture is a five-column projection of the contract above (offer_id, lander_id, payout, currency carry no weight in the bucket comparison), and its last old-side row is the case the guard handles: subid-4 has no tid, so it never enters the join and joined_rows 4 counts only keyable rows. The refusal is not silent. That row is appended to excluded_rows.csv, and the sixth counter prints how many rows the comparison never reached, which is what step 4 needs to explain. unkeyed and purged_key are the values the contract above assigns to link_class, so an unkeyable row belongs in the same inventory as the four buckets and not in a discarded stream; dropping it in silence is how a legacy tracker data migration reports a clean comparison over a subset nobody named.

Six steps, each with its own artifact:

  1. Freeze a data snapshot for the historical interval being audited and compare immutable copies. Keep production postback intake running and record late arrivals separately.
  2. Repeat the export from that same snapshot and compare the files. Investigate any differences in filters, export settings or snapshot selection before using the comparison.
  3. Run the block above as a script and keep bucket_counts.txt: six counters on six lines, written to a file rather than photographed on a terminal, with excluded_rows.csv beside it.
  4. Explain every bucket by class: purged key, unlinked row, later receipt, filter difference in the new feed, rows the empty-tid or # guard refused. An unexplained residual is a finding, not rounding.
  5. Recompute the metric you actually report (payout by offer, count by status) from both sides, and compare the metric rather than only the row counts.
  6. Record what the audit could not reach: any row whose click side had already aged out of the documented export window.

The Cutover Criterion and Its Limits

A defensible criterion is three conditions, each checkable by someone else:

  • every bucket in the comparison has a named cause, and the residual is zero rows or an explicitly listed exclusion;
  • key coverage is reported per interval and per offer, because a blended figure hides the campaigns where the source had nothing to carry;
  • the same comparison re-run after the old system stops receiving traffic reproduces the counts, so the feed - not the export script - is what you now trust.

Coverage percentages have no universal target in legacy tracker data migration: the denominator is your own rows, and any number quoted in a vendor post belongs to that vendor's campaign mix. The ladder of observations keeps the claim honest about its own level.

LevelArtifact that proves itWhat it does not prove
Sentthe partner or network's outbound logthat the tracker understood the request
Receiveda line in the tracker's own request log for that call, whichever way its decision wentthat the call produced a conversion row, because the log quoted above records a discarded request as tidily as a kept one
Accepteda conversion row keyed on click_key plus tidthat the row reaches a report, or that a later export still reaches it
Exporteda reproducible file within the export's documented accessible date range holding that rowthat the new feed counts it
Countedthe new feed or report showing the rowattribution or bidding results inside an ad platform

The last row is the real boundary, and product documentation says it in one line: integration records "do not promise attribution or bidding outcomes inside an ad platform" (Way2 Telegram tracking, evidence and boundaries). Whether a destination still accepts history at all is a separate check with its own window, covered for the Telegram contour in Telegram historical backfill feasibility and deliberately not decided here.

Two more limits follow from the inventories above. A migration must preserve the distinction between a full visit and the campaign or funnel context available for an unkeyed conversion. And a dump is not an audit: a full server transfer moves statistics and settings automatically, including the postback key, yet the receiving system still needs someone to say which time base each column holds (Keitaro: transfer to another server).

Need to review a migration?

Share an export column list and a sanitized example of an unexplained difference. The first consultation is free.

Do not send tokens or passwords.

Sources

Sources

Was this guide helpful?
Author
Most Team
Справочная служба

Официальные руководства и глоссарий для платформы Most и Активатора пикселей.

Related guides