Skip to content

[AI-FSSDK] [FSSDK-12813] Normalize decision event campaign_id, variation_id, and entity_id#455

Open
jaeopt wants to merge 4 commits into
masterfrom
ai/jaeopt/FSSDK-12813-holdout-event
Open

[AI-FSSDK] [FSSDK-12813] Normalize decision event campaign_id, variation_id, and entity_id#455
jaeopt wants to merge 4 commits into
masterfrom
ai/jaeopt/FSSDK-12813-holdout-event

Conversation

@jaeopt

@jaeopt jaeopt commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Normalize the campaign_id, variation_id, and entity_id fields on outgoing decision events uniformly across every decision type (experiment, feature test, rollout, holdout) so the SDK emits cross-SDK byte-equivalent wire output. Per the updated FSSDK-12813 spec: decisions[].campaign_id and impression events[].entity_id require a non-empty string (any character content — IDs may be opaque, e.g. "default-12345", "layer_abc") and fall back to experiment_id only when empty; decisions[].variation_id retains a strict numeric-string contract and becomes JSON null for empty / non-numeric values. The path never logs, errors, or blocks event dispatch.

Changes

  • Added pkg/event/event_id_normalizer.go with pure helpers:
    • IsNonEmptyString — relaxed predicate for campaign_id / entity_id.
    • IsNumericIDString — strict predicate, still used for variation_id.
    • NormalizeCampaignID(campaignID, experimentID) — returns campaignID when non-empty (opaque values allowed); otherwise falls back to experimentID.
    • NormalizeVariationID(variationID) *string — returns a pointer when variationID is a numeric string; otherwise nil so the field serializes as JSON null.
  • Wired the normalizer into the single impression path in pkg/event/factory.gocreateImpressionEvent normalizes from experiment.LayerID (falling back to experiment.ID only when LayerID is empty, e.g. holdouts with no layer) and reuses the same value for EntityID. createImpressionVisitor re-applies normalization as defense-in-depth so the two fields cannot drift. Conversion path is untouched.
  • Changed Decision.VariationID to *string so empty / non-numeric upstream values marshal to JSON null, matching the cross-SDK contract and the existing nullable-field convention used by DecisionMetadata.CmabUUID.
  • Updated event-builder tests for the relaxed campaign_id / entity_id contract: added TestIsNonEmptyString, TestNormalizeCampaignID_PassesThroughOpaqueCampaignID, TestNormalizeCampaignID_PassesThroughHoldoutPlaceholder, TestNormalizeCampaignID_PassesThroughWhitespaceCampaign, and TestImpressionEvent_PassesThroughOpaqueLayerID to assert that non-numeric / opaque values now pass through unchanged. Empty-LayerID holdout fallback assertions and the strict variation_id tests are unchanged.

Notes

  • Decision.VariationID changing from string to *string is a Go-source-level breaking change for any downstream consumer that constructs event.Decision literals directly (no such usages exist in this repo outside the event package itself). The on-the-wire JSON contract is unchanged in shape.
  • Non-string ID types are out of scope on Go (the fields are typed string); the spec relaxation is therefore purely a predicate change.

Jira Ticket

FSSDK-12813

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.

1 participant