Skip to content

Parse the _fbc cookie the way Meta writes it - #39

Open
loevgaard wants to merge 1 commit into
masterfrom
fix/20-fbc-cookie-format
Open

Parse the _fbc cookie the way Meta writes it#39
loevgaard wants to merge 1 commit into
masterfrom
fix/20-fbc-cookie-format

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Fixes #20

Stacked on #38.

Problem

CookieBasedFbcContext parsed the _fbc cookie with the SDK's Fbc::fromString(), which requires /^fb\.([012])\.(\d{13})\.([a-zA-Z0-9]+)$/, and swallowed the exception when it did not match. Two kinds of perfectly legitimate cookies were silently dropped:

  1. Click ids containing - or _. Real fbclid values are base64url. The bundle wrote such a value into the cookie itself and then could not read it back on the next request.
  2. Meta's five-segment format. Meta's own parameter builder, vendored transitively as facebook/capi-param-builder-php, writes fb.<idx>.<ts>.<fbclid>.<appendix> with a 2 or 8 character appendix. The strict pattern requires exactly four segments, so a cookie written by the browser pixel yielded no fbc at all.

Either way user_data.fbc was missing from server side events, with no log line and no exception. The only visible symptom is a lower Event Match Quality in Events Manager.

Change

The bundle parses the cookie itself with a pattern that accepts the base64url alphabet and an optional appendix segment, then builds the Fbc through the SDK's public API. A cookie whose creation time is in the future or predates Facebook is still rejected, because withCreationTime() asserts on it.

Unparseable and rejected cookies are now logged at debug level on the setono_meta_conversions_api Monolog channel instead of vanishing.

psr/log moves from a transitive to a declared dependency.

The SDK's own Fbc::fromString() is left alone; the bundle simply no longer depends on it for this. Relaxing it upstream in setono/meta-conversions-api-php-sdk is still worthwhile, and the appendix behaviour is worth confirming against what fbevents.js writes in a live browser.

Tests

Eleven unit tests: no request, no cookie, four accepted shapes (alphanumeric, base64url, both appendix lengths) asserting click id, subdomain index and creation time, and seven rejected shapes.

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.14%. Comparing base (762154a) to head (812c650).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master      #39      +/-   ##
============================================
+ Coverage     62.40%   66.14%   +3.74%     
- Complexity      129      131       +2     
============================================
  Files            30       30              
  Lines           375      387      +12     
============================================
+ Hits            234      256      +22     
+ Misses          141      131      -10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

*
* This is deliberately more lenient than Fbc::fromString() in the SDK, which only accepts alphanumeric click ids
* and exactly four segments. Real click ids are base64url and contain - and _, and Meta's own parameter builder
* (facebook/capi-param-builder-php) appends a 2 or 8 character appendix, so the strict pattern rejects cookies

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the facebook/capi-param-builder-php if possible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth doing, but I would not fold it into this PR. I ran it against the current vendored copy (1.3.1, it arrives transitively through facebook/php-business-sdk) to get real numbers:

$b = new FacebookAds\ParamBuilder();
$cookies = $b->processRequest('www.example.com', ['fbclid' => 'IwAR1a-b_c'], ['_fbp' => 'fb.1.1657051589577.1088522659']);

// fbc: 'fb.1.1788781160733.IwAR1a-b_c.AQECAQMB'
// fbp: 'fb.1.1657051589577.1088522659.AQEAAQMB'
// cookie _fbc domain=example.com   <- eTLD+1, not the host

That confirms two things the review found: Meta writes a five segment value with an eight character appendix, and it sets the cookie on the registrable domain rather than host-only.

What speaks for it

What speaks against doing it here

  • It would have to become a direct require. Today we only get it because php-business-sdk pulls it in.
  • It targets PHP 7.4 with no parameter or return types, so at PHPStan level max every call site needs narrowing.
  • It is a whole-request abstraction: one call computes fbc, fbp, ip and source url and returns the cookies to set. Our Context classes are small decorators a user can swap one at a time. Adopting it means redesigning Context/Fbc, Context/Fbp, StoreFbcSubscriber and StoreFbpSubscriber, and deciding what FbcContextInterface::getFbc(): ?Fbc returns, since the builder deals in strings.
  • It returns strings rather than the SDK's Fbc/Fbp value objects. User::$fbc accepts string|Fbc|null so it would work, but our own interfaces would change.

So: a good idea, and a design change rather than a bug fix. I opened #45 for it. This PR stays the minimal fix so the cookies Meta writes today are actually read, whichever way that decision goes.

The SDK's Fbc::fromString() only accepts alphanumeric click ids and
exactly four segments. Real click ids are base64url, and Meta's own
parameter builder appends an appendix segment, so cookies written by the
browser pixel were silently dropped and no fbc was sent.

Parse the cookie in the bundle with a pattern matching what Meta writes,
and log at debug level when a value cannot be used.

Fixes #20
@loevgaard
loevgaard force-pushed the fix/19-fbclid-validation branch from 097310c to 762154a Compare September 7, 2026 12:03
@loevgaard
loevgaard force-pushed the fix/20-fbc-cookie-format branch from 0e4216b to 812c650 Compare September 7, 2026 12:03
Base automatically changed from fix/19-fbclid-validation to master September 7, 2026 12:04
Comment on lines +46 to +64
if (1 !== preg_match(self::COOKIE_PATTERN, $cookie, $matches)) {
$this->logger->debug('The _fbc cookie value "{value}" could not be parsed and is ignored', ['value' => $cookie]);

return null;
}

try {
return (new Fbc($matches[3]))
->withSubdomainIndex((int) $matches[1])
->withCreationTime((int) $matches[2])
;
} catch (\InvalidArgumentException $e) {
// The creation time is in the future or predates Facebook
$this->logger->debug('The _fbc cookie value "{value}" was rejected: {message}', [
'value' => $cookie,
'message' => $e->getMessage(),
]);

return null;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why put the code here instead of rewriting Fbc::fromString?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it belongs there. I moved it: Setono/meta-conversions-api-php-sdk#13.

My reason for putting it here was that the SDK is a separate release cycle, which is a weak argument when you maintain both, and it left the bundle carrying a second parser next to the SDK's.

Chasing it into the SDK also turned up something my review missed: Fbp::fromString() has the same bug, and there it is worse.

Fbp::fromString('fb.1.1657051589577.1088522659.AQEAAQMB'); // throws

That is the shape Meta's parameter builder writes. CookieBasedFbcContext returning null costs you the click id. CookieBasedFbpContext falling through to GeneratedFbpContext means the server mints a brand new fbp on every single request while the browser has a stable one, so the two sides stop describing the same person. Nothing throws where you can see it, and the only symptom is a lower Event Match Quality.

The SDK PR relaxes both patterns and preserves the appendix, so a value read from a cookie is written back byte for byte rather than being rewritten into a shape the pixel does not expect. Coverage stays at 100% and Infection stays above the threshold.

Suggested order. This PR is blocked until that one is released, since CI here would still resolve ^1.1. Once you merge and tag the SDK, I will reduce this PR to: call Fbc::fromString() again, keep the debug logging for a cookie that cannot be parsed, fix the _fbp path the same way, and bump the constraint. That deletes more code here than it adds.

Say the word if you would rather I park this one and reopen it after the tag.

@loevgaard

Copy link
Copy Markdown
Member Author

Rebased the rest of the series off this branch, so this PR is now standalone on master. Nothing else waits on it while the SDK change lands.

#40 was sitting on this branch and is now based on master directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_fbc cookies written by Meta are not parsed: SDK regex rejects '-'/'_' and the newer 5-segment format

1 participant