Skip to content

fix(flags): keep session attribution on minimized $feature_flag_called events - #252

Merged
ioannisj merged 3 commits into
mainfrom
posthog/flag-called-session-attribution-allowlist
Oct 9, 2026
Merged

ioannisj merged 3 commits into
mainfrom
posthog/flag-called-session-attribution-allowlist

Conversation

@posthog

@posthog posthog Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

The feature-flag-called-tracker spec requires minimized $feature_flag_called events to keep session-attribution properties. Web analytics reads a session's attribution from its first event, and a minimized event can be that first event. The sdk-specs compliance matrix flags posthog-php as missing all of them.

This is reachable in PHP today: attribution set via PostHog::withContext(['properties' => ['utm_source' => ...]]) gets stripped from a gated $feature_flag_called event, nulling the whole session's attribution.

Adds to Client::MINIMAL_FLAG_CALLED_EVENT_PROPERTIES:

  • $referring_domain, the five utm_* params, gad_source and mc_cid, as named by the spec
  • the full click-id list from posthog-js MINIMAL_FLAG_CALLED_EVENT_CAMPAIGN_PROPERTIES (packages/core/src/featureFlagUtils.ts), in the same order: gclid, gclsrc, dclid, gbraid, wbraid, fbclid, msclkid, twclid, li_fat_id, igshid, ttclid, rdt_cid, epik, qclid, sccid, oppref, irclid, _kx

Full $referrer stays excluded. The change is strictly additive: minimized events can only keep more properties, and the constant is private so there's no public API change. Gating, dedupe and the fail-safe-to-full-event path are untouched.

Follow-ups:

  • posthog-js also keeps $current_url, $pathname, $window_id and $device_id. They're left out because the spec doesn't require them and they're mostly client-side.
  • posthog-python and posthog-node have the same allowlist gap.

💚 How did you test it?

testMinimalFlagCalledEventKeepsSessionAttributionProperties in test/FeatureFlagTest.php sets the spec-named attribution keys, msclkid, ttclid, _kx, $referrer and a custom property on the request context. It asserts the attribution keys survive a gated minimized event while $referrer, the custom property and $lib_consumer are stripped.

The full suite, phpcs and scripts/check-public-api.php passed on the original commit. After adding the click-id list, test/FeatureFlagTest.php passes (70 tests).

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm change to generate a change intent file

🤖 Agent context

DRI: @ioannisj
Autonomy: Human-driven (agent-assisted)

…d events

Add $referring_domain, utm_source, utm_medium, utm_campaign, utm_content,
utm_term, gad_source, mc_cid, gclid and fbclid to the minimal-event
allowlist, per the feature-flag-called-tracker spec. Web analytics reads a
session's initial attribution from the session's first event, and a
minimized $feature_flag_called event can be that first event, so stripping
these nulled out attribution for the whole session. Full $referrer stays
excluded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 9910bd77-fd45-4b67-8a88-a03de312b2ca
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-10-09T10:24:28.596511+00:00
Duration: 118880ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 29ms
Format Validation.Event Has Uuid ✅ 523ms
Format Validation.Event Has Lib Properties ✅ 523ms
Format Validation.Distinct Id Is String ✅ 524ms
Format Validation.Token Is Present ✅ 525ms
Format Validation.Custom Properties Preserved ✅ 523ms
Format Validation.Event Has Timestamp ✅ 524ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 524ms
Retry Behavior.Retries On 503 ✅ 5833ms
Retry Behavior.Does Not Retry On 400 ✅ 2527ms
Retry Behavior.Does Not Retry On 401 ✅ 2526ms
Retry Behavior.Respects Retry After Header ✅ 8529ms
Retry Behavior.Implements Backoff ✅ 16251ms
Retry Behavior.Retries On 500 ✅ 5634ms
Retry Behavior.Retries On 502 ✅ 5633ms
Retry Behavior.Retries On 504 ✅ 5635ms
Retry Behavior.Max Retries Respected ✅ 17048ms
Deduplication.Generates Unique Uuids ✅ 232ms
Deduplication.Preserves Uuid On Retry ✅ 5636ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10839ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5636ms
Deduplication.No Duplicate Events In Batch ✅ 530ms
Deduplication.Different Events Have Different Uuids ✅ 526ms
Compression.Sends Gzip When Enabled ✅ 524ms
Batch Format.Uses Proper Batch Structure ✅ 525ms
Batch Format.Flush With No Events Sends Nothing ✅ 521ms
Batch Format.Multiple Events Batched Together ✅ 510ms
Error Handling.Does Not Retry On 403 ✅ 2527ms
Error Handling.Does Not Retry On 413 ✅ 2526ms
Error Handling.Retries On 408 ✅ 5632ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 522ms
Request Payload.Flags Request Uses V2 Query Param ✅ 523ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 521ms
Request Payload.Flags Request Omits Authorization Header ✅ 522ms
Request Payload.Token In Flags Body Matches Init ✅ 522ms
Request Payload.Groups Round Trip ✅ 522ms
Request Payload.Groups Default To Empty Object ✅ 522ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 522ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 522ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 521ms
Request Lifecycle.No Flags Request On Init Alone ✅ 517ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 527ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 523ms
Retry Behavior.Retries Flags On 502 ✅ 625ms
Retry Behavior.Retries Flags On 504 ✅ 625ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 524ms

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-10-09T10:24:37.104587+00:00
Duration: 112842ms

⚠️ Some Tests Failed

36/47 tests passed, 11 failed


Capture Tests

⚠️ 19/30 tests passed, 11 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 34ms
Format Validation.Event Has Uuid ✅ 528ms
Format Validation.Event Has Lib Properties ✅ 528ms
Format Validation.Distinct Id Is String ✅ 528ms
Format Validation.Token Is Present ✅ 528ms
Format Validation.Custom Properties Preserved ✅ 528ms
Format Validation.Event Has Timestamp ✅ 528ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 527ms
Retry Behavior.Retries On 503 ❌ 5531ms
Retry Behavior.Does Not Retry On 400 ✅ 2533ms
Retry Behavior.Does Not Retry On 401 ✅ 2531ms
Retry Behavior.Respects Retry After Header ❌ 5536ms
Retry Behavior.Implements Backoff ❌ 15544ms
Retry Behavior.Retries On 500 ❌ 5535ms
Retry Behavior.Retries On 502 ❌ 5534ms
Retry Behavior.Retries On 504 ❌ 5538ms
Retry Behavior.Max Retries Respected ❌ 15544ms
Deduplication.Generates Unique Uuids ✅ 540ms
Deduplication.Preserves Uuid On Retry ❌ 5532ms
Deduplication.Preserves Uuid And Timestamp On Retry ❌ 10539ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ❌ 5540ms
Deduplication.No Duplicate Events In Batch ✅ 535ms
Deduplication.Different Events Have Different Uuids ✅ 530ms
Compression.Sends Gzip When Enabled ✅ 531ms
Batch Format.Uses Proper Batch Structure ✅ 528ms
Batch Format.Flush With No Events Sends Nothing ✅ 517ms
Batch Format.Multiple Events Batched Together ✅ 520ms
Error Handling.Does Not Retry On 403 ✅ 2529ms
Error Handling.Does Not Retry On 413 ✅ 2531ms
Error Handling.Retries On 408 ❌ 5532ms

Failures

retry_behavior.retries_on_503

Expected at least 3 requests, got 1

retry_behavior.respects_retry_after_header

Expected at least 2 requests, got 1

retry_behavior.implements_backoff

Expected at least 3 requests, got 1

retry_behavior.retries_on_500

Expected at least 2 requests, got 1

retry_behavior.retries_on_502

Expected at least 2 requests, got 1

retry_behavior.retries_on_504

Expected at least 2 requests, got 1

retry_behavior.max_retries_respected

Expected 4 requests, got 1

deduplication.preserves_uuid_on_retry

Need at least 2 requests to check retry

deduplication.preserves_uuid_and_timestamp_on_retry

Expected at least 3 requests, got 1

deduplication.preserves_uuid_and_timestamp_on_batch_retry

Expected at least 2 requests, got 1

error_handling.retries_on_408

Expected at least 2 requests, got 1

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 524ms
Request Payload.Flags Request Uses V2 Query Param ✅ 520ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 520ms
Request Payload.Flags Request Omits Authorization Header ✅ 521ms
Request Payload.Token In Flags Body Matches Init ✅ 519ms
Request Payload.Groups Round Trip ✅ 519ms
Request Payload.Groups Default To Empty Object ✅ 521ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 522ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 521ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 520ms
Request Lifecycle.No Flags Request On Init Alone ✅ 515ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 518ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 525ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 522ms
Retry Behavior.Retries Flags On 502 ✅ 623ms
Retry Behavior.Retries Flags On 504 ✅ 625ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 531ms

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-10-09T10:25:03.566423+00:00
Duration: 149280ms

⚠️ Some Tests Failed

36/47 tests passed, 11 failed


Capture Tests

⚠️ 19/30 tests passed, 11 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 31ms
Format Validation.Event Has Uuid ✅ 524ms
Format Validation.Event Has Lib Properties ✅ 524ms
Format Validation.Distinct Id Is String ✅ 525ms
Format Validation.Token Is Present ✅ 525ms
Format Validation.Custom Properties Preserved ✅ 525ms
Format Validation.Event Has Timestamp ✅ 524ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 526ms
Retry Behavior.Retries On 503 ❌ 9234ms
Retry Behavior.Does Not Retry On 400 ✅ 2528ms
Retry Behavior.Does Not Retry On 401 ✅ 2529ms
Retry Behavior.Respects Retry After Header ❌ 9235ms
Retry Behavior.Implements Backoff ❌ 19234ms
Retry Behavior.Retries On 500 ❌ 8751ms
Retry Behavior.Retries On 502 ❌ 9237ms
Retry Behavior.Retries On 504 ❌ 9237ms
Retry Behavior.Max Retries Respected ❌ 19247ms
Deduplication.Generates Unique Uuids ✅ 534ms
Deduplication.Preserves Uuid On Retry ❌ 9235ms
Deduplication.Preserves Uuid And Timestamp On Retry ❌ 14241ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ❌ 9240ms
Deduplication.No Duplicate Events In Batch ✅ 532ms
Deduplication.Different Events Have Different Uuids ✅ 526ms
Compression.Sends Gzip When Enabled ✅ 525ms
Batch Format.Uses Proper Batch Structure ✅ 525ms
Batch Format.Flush With No Events Sends Nothing ✅ 521ms
Batch Format.Multiple Events Batched Together ✅ 512ms
Error Handling.Does Not Retry On 403 ✅ 2527ms
Error Handling.Does Not Retry On 413 ✅ 2528ms
Error Handling.Retries On 408 ❌ 5530ms

Failures

retry_behavior.retries_on_503

Expected at least 3 requests, got 1

retry_behavior.respects_retry_after_header

Expected at least 2 requests, got 1

retry_behavior.implements_backoff

Expected at least 3 requests, got 1

retry_behavior.retries_on_500

Expected at least 2 requests, got 1

retry_behavior.retries_on_502

Expected at least 2 requests, got 1

retry_behavior.retries_on_504

Expected at least 2 requests, got 1

retry_behavior.max_retries_respected

Expected 4 requests, got 1

deduplication.preserves_uuid_on_retry

Need at least 2 requests to check retry

deduplication.preserves_uuid_and_timestamp_on_retry

Expected at least 3 requests, got 1

deduplication.preserves_uuid_and_timestamp_on_batch_retry

Expected at least 2 requests, got 1

error_handling.retries_on_408

Expected at least 2 requests, got 1

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 525ms
Request Payload.Flags Request Uses V2 Query Param ✅ 523ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 522ms
Request Payload.Flags Request Omits Authorization Header ✅ 523ms
Request Payload.Token In Flags Body Matches Init ✅ 523ms
Request Payload.Groups Round Trip ✅ 523ms
Request Payload.Groups Default To Empty Object ✅ 522ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 523ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 523ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 523ms
Request Lifecycle.No Flags Request On Init Alone ✅ 518ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 527ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 524ms
Retry Behavior.Retries Flags On 502 ✅ 625ms
Retry Behavior.Retries Flags On 504 ✅ 626ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 525ms

@marandaneto
marandaneto requested a review from a team October 6, 2026 07:19
@ioannisj ioannisj self-assigned this Oct 7, 2026
ioannisj
ioannisj previously approved these changes Oct 7, 2026

@ioannisj ioannisj left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Premise: this matches the sdk-specs PHP gap and the posthog-js shape for keeping session-attribution on minimized $feature_flag_called events while still excluding $referrer.

@ioannisj
ioannisj marked this pull request as ready for review October 7, 2026 08:26
@ioannisj
ioannisj requested a review from a team as a code owner October 7, 2026 08:26
@ioannisj
ioannisj dismissed their stale review October 8, 2026 13:01

Adding the full js campaign click-id list before approving.

@ioannisj
ioannisj self-requested a review October 8, 2026 13:05
@ioannisj

ioannisj commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

🤖 An AI agent wrote this on behalf of @ioannisj.

Pushed cb8c70d to match the posthog-js click-id list (MINIMAL_FLAG_CALLED_EVENT_CAMPAIGN_PROPERTIES in packages/core/src/featureFlagUtils.ts). That adds the 16 params we were missing, gclsrc through _kx, in the same order as js. The test now also covers msclkid, ttclid and _kx, and the changeset is updated.

I left $current_url, $pathname, $window_id and $device_id out. js keeps them, but the spec doesn't ask for them and they're mostly client-side, so I think they're better as a follow-up if we want them at all.

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

seems reasonable though very edge case-y as i don't think UTM parameters are included anywhere unless user specified

Comment thread lib/Client.php
* stays out; only $referring_domain and the bare campaign/click-id keys survive. The
* campaign/click-id keys mirror posthog-js MINIMAL_FLAG_CALLED_EVENT_CAMPAIGN_PROPERTIES.
*/
private const MINIMAL_FLAG_CALLED_EVENT_PROPERTIES = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we reallocate this array on every event, can consider switching it to a map (apparently PHP doesn't have sets) to avoid calling array_flip() and reallocating

 private const MINIMAL_FLAG_CALLED_EVENT_PROPERTIES = [
    /* ... */
    '$feature_flag' => true,
    'utm_source' => true,
 ];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 An AI agent wrote this on behalf of @ioannisj.

Switched it to a keyed map in 60c5763, so the allowlist goes straight into array_intersect_key without the array_flip. Only gated $feature_flag_called events hit this path, but it was cheap to tidy.

@ioannisj
ioannisj merged commit dadaa28 into main Oct 9, 2026
26 checks passed
@ioannisj
ioannisj deleted the posthog/flag-called-session-attribution-allowlist branch October 9, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants