Consent · engineers
Two gdpr_consent parameters is not belt and suspenders
Ad servers that wrap a publisher tag and add their own consent macros produce this. The live URL looks thorough and is ambiguous.
What core checks
The TCF and GPP specs tell URL creators to add each signal exactly once. Pixellint watches gdpr, gdpr_consent, us_privacy, gpp, and gpp_sid. A repeated name is a duplicate signal. The values do not even have to disagree to be a problem. Two identical TC Strings are still two parameters. Keep one, drop the rest.
That is core.privacy.duplicate_signal. It is a warning because the callee might pick the copy you meant. It is still a miss because the callee might pick the other copy. A wrapper that appends gdpr=1 while the inner tag already has gdpr=0 is how a hop claims both applies and does not.
https://example.com/px?id=1&gdpr=1&gdpr=0&gdpr_consent=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA
Query plus path counts
Core reads query string and Floodlight-style path parameters. A tag that puts gdpr=1 on the path and gdpr=1 on the query has duplicated the flag. Wrappers that merge a publisher pixel and a brand pixel without collapsing keys are the usual factory for this.
semicolon on doubleclick.net is still a parameter. Do not argue that path keys are a different namespace. If extract_params sees the name twice, the finding fires. Collapse in the template so the ad server only expands one.
https://ad.doubleclick.net/ddm/activity/src=1234567;type=convr0;cat=purch0;ord=1;gdpr=1;gdpr_consent=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA?gdpr=1
How it happens
GTM extra query params plus the Floodlight tag's own macros. A wrapper pixel that appends what the inner pixel already has. A CMP that writes both TCF and GPP into the same key. The tag had gdpr_consent from the CMP. The ad server appended gdpr_consent from a macro. One might be a real TC String. One might be 1.
If you see this only on some hops of a redirect chain, a wrapper added the second copy mid-chain. Fix the hop. Do not add a third copy as a tie-break. The callee should not be guessing, and Pixellint should see a single decoded string.
Different signals are not duplicates
gdpr_consent and gpp together are two frameworks. us_privacy next to gpp is a migration. dpo=LDU next to us_privacy is Meta plus IAB. Delete the second copy of the same key. Do not delete GPP because you already have a TC String.
gpp_sid is its own key. One gpp_sid with two comma-separated ids is the documented way to name at most two sections in force. Do not encode that as two query keys. gpp_sid=2,6 is one parameter. gpp_sid=2&gpp_sid=6 is a duplicate. The spec allows at most two ids in one value. It does not allow two keys.
https://example.com/px?id=1&gpp=DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN&gpp_sid=2,6&us_privacy=1YNN
Fix
One writer per parameter. Inspect the fired URL. Delete the extra macro in the wrapper, not in the CMP, if the wrapper is the duplicator. Empty values and unexpanded macros do not get format-checked as strings, but a duplicate key is still a duplicate key. ${GDPR_CONSENT} twice is two parameters. Collapse them in the template so the ad server only expands one.
pixellint validate url on the live hop. If duplicate_signal and tc_string_version both fire, fix the placeholder copy first, then collapse keys. A URL with gdpr_consent=1 and gdpr_consent=<real string> is the worst version of thorough. The callee might believe 1.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.