The Measurement Protocol is how a server tells GA4 that something happened: a purchase the browser never saw, a refund, a lead that closed in the CRM. The collect endpoint returns 204. The event does not show up, or it shows up with no revenue, or it shows up attached to nobody.
A lot of that is the timestamp.
{
"client_id": "1234567.7654321",
"timestamp_micros": 1770000000000,
"events": [{ "name": "purchase", "params": { "currency": "USD", "value": 129.99 } }]
}
timestamp_micros is a Unix timestamp in microseconds. Sixteen digits. Date.now() is milliseconds, thirteen digits. Ten digits is seconds. Google's field name is doing the work; the value people actually send is the JavaScript default.
timestamp_micros: Date.now() * 1000
I maintain Pixellint, an open source linter for pixels and measurement payloads. The GA4 pack (vendor/google-analytics) contracts Measurement Protocol query parameters and the JSON body against Google's own reference. This is the finding on a 13-digit timestamp:
$ pixellint validate json @mp.json
rulepack: vendor/google-analytics (vendor: google)
error vendor.google-analytics.body.timestamp_micros.invalid
`timestamp_micros` must be at least 16 digits, but `1770000000000`
has 13. Google documents this as a Unix timestamp in microseconds,
not milliseconds. A 13-digit value is milliseconds and a 10-digit
value is seconds.
fix: Multiply a millisecond timestamp by 1000, or a second timestamp
by 1000000.
docs: https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference
Meta's Conversions API is the inverse clock: event_time is seconds, so the same Date.now() is too large rather than too small. There is no shared "timestamp" across these APIs. There is a unit per vendor.
A purchase without transaction_id is not revenue
Google's recommended purchase event needs currency, value, transaction_id, and items. A body that only has value and currency is a JSON object GA4 will accept and a purchase it will not report correctly.
{
"client_id": "1234567.7654321",
"events": [{
"name": "purchase",
"params": {
"currency": "USD",
"value": 1.0,
"items": [{ "item_id": "a" }]
}
}]
}
transaction_id is missing. The pack flags the event, not the envelope:
error vendor.google-analytics.body.purchase_requires_ecommerce_fields
A `purchase` event is missing fields Google documents as required
for it, so it will not report revenue correctly.
fix: Add the required ecommerce parameters to the event.
refund needs currency, value, and transaction_id. add_to_cart and begin_checkout need currency, value, and items. Same shape of rule, different required set.
Value without currency is its own error, on any event, because Google documents currency as required whenever value is set and drops the revenue otherwise:
"params": { "value": 10.0 }
error vendor.google-analytics.body.value_requires_currency
The event carries a `value` with no `currency`. Google documents
currency as required whenever value is set, and drops the revenue
otherwise.
Events with no client_id never join a user
client_id is recommended rather than required. The collect endpoint will take the hit. Nothing joins it to a browser, a user, or a session. In GA4 that looks like an event that exists in DebugView (sometimes) and vanishes from user reports.
warning vendor.google-analytics.body.client_id.missing
`client_id` is expected on Google Analytics 4 Measurement Protocol
requests but is not present. It identifies the browser or device
the event belongs to, and events without it are not joined to a user.
fix: Send the `_ga` cookie's client id, or a stable id of your own.
Warnings do not fail a pixellint validate run. This one should fail your run, because an orphan MP event is how server-side GA4 gets a reputation for "not matching the pixel."
On the query string, the stream identity is stricter. The request needs exactly one of measurement_id (a G- web stream) or firebase_app_id (an app stream). Missing both is an error. Sending both is an error.
non_personalized_ads still shows up in copied snippets. Google marks it deprecated in favor of the consent object. The pack warns; it does not invent a replacement value.
What the CLI actually prints
The pack matches google-analytics.com/mp/collect (and the debug and regional variants) on a URL, and it matches a JSON body that looks like Measurement Protocol even when you have not got the URL yet.
pixellint validate json @mp.json
pixellint validate url 'https://www.google-analytics.com/mp/collect?measurement_id=G-XXXX&api_secret=SECRET'
rulepack: core
ok
rulepack: vendor/google-analytics (vendor: google)
error vendor.google-analytics.body.timestamp_micros.invalid ...
error vendor.google-analytics.body.purchase_requires_ecommerce_fields ...
2 error(s), 0 warning(s), 0 info message(s) across 2 rulepack(s).
cargo install pixellint
# or: npm install pixellint
The contracts, with citations, live on the GA4 Measurement Protocol pack page. Paste a body into the browser playground if you do not want a clone. Nothing you paste leaves the browser.
The short version
timestamp_micros is 16 digits. Multiply Date.now() by 1000. A purchase needs currency, value, transaction_id, and items. Value never travels without currency. Send a client_id or the event belongs to nobody.
Pixellint is independent of Google. The rule ids above cite the Measurement Protocol reference because that is where the requirements live, not because this is an official tool.
Top comments (0)