Skip to content

Fix/optimizely personalization campaign#1463

Merged
Matus Tomlein (matus-tomlein) merged 3 commits intosnowplow:fix/optimizely-personalization-campaign-idfrom
JamesShebester:fix/optimizely-personalization-campaign-id
Apr 20, 2026
Merged

Fix/optimizely personalization campaign#1463
Matus Tomlein (matus-tomlein) merged 3 commits intosnowplow:fix/optimizely-personalization-campaign-idfrom
JamesShebester:fix/optimizely-personalization-campaign-id

Conversation

@JamesShebester
Copy link
Copy Markdown
Contributor

Summary

Fixes #1462

Replaces getActiveExperimentIds() with getCampaignStates({ isActive: true }) in getOptimizelyXSummary() to correctly capture campaignId for Optimizely Personalization campaigns.

Problem

The existing implementation uses two Optimizely state API calls that
bypass the campaign layer entirely:

// Before — misses campaignId completely
var experiment_ids = state.getActiveExperimentIds() || [];
var variationMap = state.getVariationMap();

For Optimizely Personalization campaigns, getActiveExperimentIds()
surfaces experience IDs (the sub-unit inside a campaign) but never
exposes the parent campaignId (layerId). This means campaignId
was never captured in Snowplow event data — not as null, not as empty — it simply did not exist in the schema output.

Solution

getCampaignStates({ isActive: true }) returns a unified state object
for both Web Experimentation and Personalization campaigns, where the
top-level key is the campaignId:

// After — correctly captures campaignId for all campaign types
const campaignStates = state.getCampaignStates({ isActive: true }) || {};

return Object.keys(campaignStates).map((campaignId: string) => {
  const campaign = campaignStates[campaignId];
  return {
    campaignId: parseAndValidateInt(campaignId) || null,
    experimentId: parseAndValidateInt(campaign.experiment?.id) || null,
    variationName: campaign.variation?.name?.toString() || null,
    variation: parseAndValidateInt(campaign.variation?.id) || null,
    visitorId: visitorId,
  };
});

Behavior by Campaign Type

Field Web Experimentation Personalization
campaignId layerId (auto-generated wrapper) layerId of the campaign
experimentId experimentId experienceId (sub-unit of campaign)
variation variationId variationId

Changes

  • src/index.ts — replaced getActiveExperimentIds() + getVariationMap()
    with getCampaignStates({ isActive: true }), moved visitorId resolution
    outside the loop, updated schema reference to 1-1-0
  • src/contexts.ts — added campaignId?: number | null to
    OptimizelyxSummary interface

Schema

This change requires a new schema version
iglu:com.optimizely.optimizelyx/summary/jsonschema/1-1-0 with campaignId
added as an optional integer field. This is a minor version bump
(1-0-01-1-0) as the addition is backward compatible — existing events without campaignId remain valid.

Testing

Verified in browser console against a live Optimizely Personalization
campaign:

optimizely.get('state').getCampaignStates({ isActive: true })
// Returns campaignId, experiment.id, variation.id, and variation.name 
// correctly for both Personalization and Web Experimentation campaigns

…campaign

getActiveExperimentIds() does not surface campaignId (layerId) for
   Optimizely Personalization campaigns. Switching to getCampaignStates()
   correctly captures the campaign layer for both Web Experimentation
   and Personalization campaigns. Also adds campaignId to OptimizelyxSummary
   interface and bumps schema reference to 1-1-0.
@snowplowcla
Copy link
Copy Markdown

Thanks for your pull request. Is this your first contribution to a Snowplow open source project? Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://docs.snowplowanalytics.com/docs/contributing/contributor-license-agreement/ to learn more and sign.

Once you've signed, please reply here (e.g. I signed it!) and we'll verify. Thanks.

@snowplowcla Snowplow CLA bot (snowplowcla) added the cla:no [Auto generated] Snowplow Contributor License Agreement has not been signed. label Mar 26, 2026
@JamesShebester
Copy link
Copy Markdown
Contributor Author

@snowplowcla
Copy link
Copy Markdown

Confirmed! James Shebester (@JamesShebester) has signed the Contributor License Agreement. Thanks so much.

@snowplowcla Snowplow CLA bot (snowplowcla) added cla:yes [Auto generated] Snowplow Contributor License Agreement has been signed. and removed cla:no [Auto generated] Snowplow Contributor License Agreement has not been signed. labels Mar 26, 2026
@johnmicahreid
Copy link
Copy Markdown

Thanks for this contribution James Shebester (@JamesShebester)!

We'll take a look and get the Iglu Central PR raised on our side for the schema version bump.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the Optimizely X browser plugin to correctly include campaignId (layerId) in the emitted Optimizely summary context, especially for Optimizely Personalization campaigns where getActiveExperimentIds() does not expose the campaign layer.

Changes:

  • Switch summary extraction to state.getCampaignStates({ isActive: true }) and include campaignId in the mapped context data.
  • Extend the OptimizelyxSummary TypeScript interface to include campaignId.
  • Bump the emitted Iglu schema reference from 1-0-0 to 1-1-0.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

File Description
plugins/browser-plugin-optimizely-x/src/index.ts Uses getCampaignStates to map active campaigns and emits schema 1-1-0.
plugins/browser-plugin-optimizely-x/src/contexts.ts Adds campaignId to the summary context interface.
plugins/browser-plugin-optimizely-x/pnpm-lock.yaml Introduces a per-package lockfile (conflicts with Rush-managed lockfile conventions).
Files not reviewed (1)
  • plugins/browser-plugin-optimizely-x/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +16
dependencies:
'@snowplow/browser-tracker-core':
specifier: ~4.6.9
version: 4.6.9
'@snowplow/tracker-core':
specifier: ~4.6.9
version: 4.6.9
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The dependency entries in this lockfile pin @snowplow/* to ~4.6.9, which conflicts with this package's package.json (workspace:* deps and peer @snowplow/browser-tracker ~4.7.0). If this lockfile is kept, it should be regenerated from the monorepo install so versions/specifiers match the workspace configuration; otherwise remove it.

Copilot uses AI. Check for mistakes.
* Schema for an Optimizely X summary context
*/
export interface OptimizelyxSummary {
campaignId?: number | null; // added: captures the Personalization campaign layerId
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This inline comment implies campaignId is only for Personalization, but index.ts now populates campaignId for all campaign types (Web Experimentation + Personalization). Update the comment to avoid misleading future readers.

Suggested change
campaignId?: number | null; // added: captures the Personalization campaign layerId
campaignId?: number | null; // captures the campaign/layer identifier for Web Experimentation and Personalization

Copilot uses AI. Check for mistakes.
Comment on lines 109 to 112
return getOptimizelyXSummary().map(function (experiment) {
return {
schema: 'iglu:com.optimizely.optimizelyx/summary/jsonschema/1-0-0',
schema: 'iglu:com.optimizely.optimizelyx/summary/jsonschema/1-1-0',
data: experiment,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The callback parameter name experiment is now a bit misleading since each element can represent a campaign summary (it includes campaignId). Renaming it to something like summary/contextData would improve readability and avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +67
lockfileVersion: '9.0'

settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

importers:

.:
dependencies:
'@snowplow/browser-tracker-core':
specifier: ~4.6.9
version: 4.6.9
'@snowplow/tracker-core':
specifier: ~4.6.9
version: 4.6.9

packages:

'@snowplow/browser-tracker-core@4.6.9':
resolution: {integrity: sha512-KyW/zU7Isb49/2XcliwTb7yOZvowHXBfdlODiEOGXWn9QcWyAEawqETh/gZJua/+lg+9d6zp2F9bqkKPpwtpKg==}

'@snowplow/tracker-core@4.6.9':
resolution: {integrity: sha512-ybKK3t7pd+JwX9BA+DeiuyzbOgE0RpbzhTpPwHvIom+gAQUYCZpyECxFT5FBLrdZtGeWdIaoY93JXneDNfVGLg==}

charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}

crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}

sha1@1.1.1:
resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==}

tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}

uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true

snapshots:

'@snowplow/browser-tracker-core@4.6.9':
dependencies:
'@snowplow/tracker-core': 4.6.9
sha1: 1.1.1
tslib: 2.8.1
uuid: 10.0.0

'@snowplow/tracker-core@4.6.9':
dependencies:
tslib: 2.8.1
uuid: 10.0.0

charenc@0.0.2: {}

crypt@0.0.2: {}

sha1@1.1.1:
dependencies:
charenc: 0.0.2
crypt: 0.0.2

tslib@2.8.1: {}

uuid@10.0.0: {}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This repo uses Rush with a single pnpm lockfile under common/config/rush/pnpm-lock.yaml (see .github workflows hashing that file). Committing an additional per-package pnpm-lock.yaml here is likely unused by CI and can cause confusing, divergent dependency installs if someone runs pnpm in this folder. Consider removing this file and relying on the Rush-managed lockfile instead.

Suggested change
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@snowplow/browser-tracker-core':
specifier: ~4.6.9
version: 4.6.9
'@snowplow/tracker-core':
specifier: ~4.6.9
version: 4.6.9
packages:
'@snowplow/browser-tracker-core@4.6.9':
resolution: {integrity: sha512-KyW/zU7Isb49/2XcliwTb7yOZvowHXBfdlODiEOGXWn9QcWyAEawqETh/gZJua/+lg+9d6zp2F9bqkKPpwtpKg==}
'@snowplow/tracker-core@4.6.9':
resolution: {integrity: sha512-ybKK3t7pd+JwX9BA+DeiuyzbOgE0RpbzhTpPwHvIom+gAQUYCZpyECxFT5FBLrdZtGeWdIaoY93JXneDNfVGLg==}
charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
sha1@1.1.1:
resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
snapshots:
'@snowplow/browser-tracker-core@4.6.9':
dependencies:
'@snowplow/tracker-core': 4.6.9
sha1: 1.1.1
tslib: 2.8.1
uuid: 10.0.0
'@snowplow/tracker-core@4.6.9':
dependencies:
tslib: 2.8.1
uuid: 10.0.0
charenc@0.0.2: {}
crypt@0.0.2: {}
sha1@1.1.1:
dependencies:
charenc: 0.0.2
crypt: 0.0.2
tslib@2.8.1: {}
uuid@10.0.0: {}

Copilot uses AI. Check for mistakes.
@matus-tomlein Matus Tomlein (matus-tomlein) changed the base branch from release/4.8.0 to fix/optimizely-personalization-campaign-id April 20, 2026 07:52
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.

Thank you for the contribution James Shebester (@JamesShebester)! We will work on getting the schema published and finalizing this into a tracker release on our side.

@matus-tomlein Matus Tomlein (matus-tomlein) merged commit 653f446 into snowplow:fix/optimizely-personalization-campaign-id Apr 20, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla:yes [Auto generated] Snowplow Contributor License Agreement has been signed.

Development

Successfully merging this pull request may close these issues.

bug: browser-plugin-optimizely-x does not capture campaignId for Optimizely Personalization campaigns

5 participants