DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

GA4 Measurement Protocol Wants Microseconds. Date.now() Is Milliseconds.

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 } }]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }]
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode
  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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode
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).
Enter fullscreen mode Exit fullscreen mode
cargo install pixellint
# or: npm install pixellint
Enter fullscreen mode Exit fullscreen mode

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)