pixellint

Identity · engineers · PMs

client_id is analytics. external_id is yours.

Three systems, three join keys, one warehouse column people keep reusing. The names sound like synonyms; the graphs they attach to are not.

GA4 client_id

Measurement Protocol client_id is the browser or device id GA4 already knows, usually from the _ga cookie. Events without it still ingest. They never join a user. Pixellint flags a missing client_id as vendor.google-analytics.body.client_id.missing. That code is recommended-severity in the sense that the collect endpoint will take the hit anyway. Treat it as a fail in your run. cid on /g/collect is the same idea on the wire.

App streams use app_instance_id instead of client_id, and firebase_app_id instead of measurement_id. Sending the web client_id to an app stream, or both stream identifiers on one request, is how hits land on nobody. Pixellint errors if you send both measurement_id and firebase_app_id (vendor.google-analytics.stream.identifier_ambiguous) or neither (identifier_missing).

client_id looks like 1234567890.1234567890 (two integers, a dot). Do not put the Meta _fbp in that field. Do not put your CRM id in that field unless you also set that same value as the GA4 user_id you intended, which is a different key.

Firebase app_instance_id

app_instance_id identifies an app install instance on a GA4 app stream. It is not the web client_id. It is not the Firebase installation id you might log for Cloud Messaging, unless you have confirmed they are the same in your SDK version. Send app_instance_id with firebase_app_id. Send client_id with measurement_id. Mixing them is a silent empty user.

A shared MP helper that always sets client_id will look correct on web and attach app events to nobody. Branch on stream type. The warehouse column named client_id is not a hint that every vendor wants it.

Meta external_id

user_data.external_id is an identifier you own: CRM id, login id, account id. Meta recommends SHA-256 hashing it. Pixellint requires the SHA-256 hex shape on that field. It is not client_id, not _fbp, and not the email in disguise. If the value is the email, you have not created a separate key. Hashing the email into both em and external_id still gives Meta one fact, twice.

Use the same external_id on the pixel and on CAPI if you send it on both. Rotating it per event makes the graph worse, not better. Rotating it per session is a new person every visit. Stable customer key, hashed the same way every time, including after password reset.

Amplitude is another pair

Amplitude HTTP V2 wants user_id or device_id, five characters or more, or it drops them. Pixellint flags shorter ids as vendor.amplitude.body.user_id.invalid and .device_id.invalid, and flags an event with neither as event_needs_an_identifier. That is not GA4 client_id and not Meta external_id.

Map your customer key onto user_id, map a stable device key onto device_id, and keep a dictionary so the next vendor does not inherit the wrong column. A shared pipeline that names every field id will ship. It will not join. user_id: '42' is four characters and Amplitude will drop it unless min_id_length is set. Pad or namespace it (acct_42), do not silently send it.

Write the dictionary

One row per vendor field: GA4 client_id, GA4 user_id, GA4 app_instance_id, Meta external_id (hashed), Amplitude user_id, Amplitude device_id, PostHog distinct_id. Source column, hash or not, min length, example value. Without that table, every new destination copies the last destination's key.

Pixellint is not affiliated with Google, Meta, or Amplitude. It will not map keys for you. It will tell you when client_id is missing on MP, when external_id is not 64 hex, and when Amplitude ids are too short. Run pixellint validate json on each vendor body, not on a combined blob.

// WRONG: one 'id' reused
ga4.client_id = crmId;
meta.user_data.external_id = ga4.client_id;
amplitude.user_id = meta.user_data.fbp;

// RIGHT: named keys
ga4.client_id = gaCookieClientId;
ga4App.app_instance_id = firebaseAppInstanceId;
meta.user_data.external_id = sha256hex(crmId);
amplitude.user_id = crmId; // >= 5 chars

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.