Data Manager uploads offline conversions straight from a query — and became a product you can find
Data Manager had half of what it needed: Customer Match audiences, but not conversion events. Both halves now exist, and the interesting part is the row source — a query result is not a file, so forcing it through one would add a writable bucket to a pipeline that otherwise needs none.
What shipped
- `upload_conversion_events` and `validate_conversion_events` — offline conversions into Google Ads through Data Manager.
- A query result as the row source. `rows` takes a SELECT's own output, so `bigquery.run_query` → upload is two steps with nothing in between. No bucket, no export, no file.
- Its own integration card, naming both capabilities and honest that it rides on the Google Ads grant as a scope union.
- Timestamps in four shapes — RFC3339, epoch seconds, epoch millis, and BigQuery's own `2026-09-15 10:00:00 UTC`.
Rows that can never be attributed are dropped, not sent
An event carrying neither a click id nor a hashed identifier can never be attributed to anything. Google accepts it and reports nothing — the silent-failure shape this whole module exists to prevent — so those rows are counted and dropped rather than uploaded.
A conversion time that cannot be parsed is refused rather than coerced to the epoch, for the same reason: a timestamp silently set to 1970 is worse than a rejected row, because nothing downstream reveals it.
Three things a live test found that reading could not
- `eventSource` is required, although the v1 discovery doc marks it an ordinary optional field. It is refused rather than defaulted — "where did this conversion happen" is a data-quality decision, and stamping every warehouse batch as WEB would mislabel offline data in a way no downstream report reveals.
- A conversion keyed by a hashed identifier is an Enhanced Conversion for Leads and needs the account enabled for it; one keyed by a click id needs nothing. One such row fails the whole batch, so the refusal now counts which rows they are.
- "Resource not found" on a conversion action that plainly exists turned out to be propagation: a newly created action answered that for around 20 minutes, then accepted a byte-identical request. Told as a dead end it reads "wrong id"; told as propagation it reads "wait".
Two corrections worth stating plainly
- The first row is not the schema. An inline batch whose first row had no `email` key silently dropped the row that did have one, because the column list came from `rows[0]` alone. Headers are now the union over a bounded sample. A warehouse SELECT is uniform, so this would have hidden for a long time.
- A dry run cannot validate Customer Match terms. Google validates the terms declaration even for a preview, so that tool had been answering 400 since it shipped. The lane now splits honestly: the genuinely local part — how many rows survive normalization and why the rest did not — is computed without asking Google, and says so.
Frequently asked questions
- Can I upload offline conversions from BigQuery without exporting a file?
- Yes. The rows argument takes a SELECT's own output, so a BigQuery query followed by an upload is two steps with nothing in between — no bucket, no CSV, no export step.
- What happens to rows that cannot be attributed?
- They are counted and dropped rather than sent. An event with neither a click id nor a hashed identifier can never be attributed, and Google accepts it while reporting nothing — which is the silent failure this module exists to prevent.
- Why does my upload fail with "Resource not found" on a conversion action that exists?
- Most likely propagation. A newly created conversion action was measured answering that for around 20 minutes before accepting a byte-identical request. It is no longer reported as a permanent failure.
- Do I need anything extra to upload conversions keyed by email?
- Yes — a conversion keyed by a hashed identifier is an Enhanced Conversion for Leads and the account has to be enabled for it. One keyed by a click id needs nothing extra. A single such row fails the whole batch, so the refusal names which rows they are.
- Are customer identifiers hashed before upload?
- Yes, identifiers are hashed before anything leaves. Data Manager also has its own OAuth scope, requested as a union with the Google Ads grant.