fix(personalize,import): fix CT linking failure from empty-audience experiences and missing variant entry data file #280
fix(personalize,import): fix CT linking failure from empty-audience experiences and missing variant entry data file #280cs-raj wants to merge 7 commits into
Conversation
…xperiences and missing variant entry data file [DX-9469] Two related bugs in the Personalize import pipeline: 1. experiences.ts — pendingVariantAndVariantGrpForExperience was populated with ALL created experience UIDs regardless of whether valid variants were submitted to the API. Experiences with variants:[] never get variant groups from the Personalize backend, so the validation loop timed out and set importData=false, skipping attachCTsInExperience for every experience including valid ones. Fixed by changing importExperienceVersions to return boolean and only adding UIDs to the pending set when valid variants were actually submitted. Also adds a null guard in attachCTsInExperience before accessing variantGroup to prevent a silent crash. 2. entries.ts — a bare return inside the no-environments-file branch of the entries import function exited the entire function, skipping createEntryDataForVariantEntry(). This meant data-for-variant-entry.json was never written whenever a stack had no environments, causing the variant-entries module to warn about the missing file and skip all variant entry imports. Fixed by removing the return. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…DX-9469 Revert changes to audiences.test.ts, variant-entries.test.ts, and package.json (test script narrowing) — these were workarounds for the ESM/uuid runner issue and are not part of the DX-9469 fix scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
Consider reviewing these vulnerabilities when fixes become available. |
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
Consider reviewing these vulnerabilities when fixes become available. |
…h empty envs [DX-9469] After removing the bare return, publishEntries is called even when no environments exist — serializePublishEntries handles empty envs as a no-op. Updated test assertion to reflect this and added assertion that createEntryDataForVariantEntry always runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
Consider reviewing these vulnerabilities when fixes become available. |
There was a problem hiding this comment.
Pull request overview
Fixes two import-pipeline bugs that could (a) prevent Content Type ↔ Variant Group linking across an entire Personalize import when some experiences have no valid variants, and (b) skip generating the variant-entry data file when the stack has no environments—causing variant entry imports to be skipped.
Changes:
- Personalize experiences import: only poll/validate variant-group creation for experiences where at least one valid version/variant was actually submitted; add a guard when variant groups are missing during CT attachment.
- Entries import: remove the early return on missing environments so
createEntryDataForVariantEntry()always runs. - Add focused unit tests and mock fixtures covering empty-audience / lytics-only / mixed / missing-versions-file scenarios.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/contentstack-variants/src/import/experiences.ts | Tracks only experiences with valid variants for validation polling; adds null-guard before CT attachment to missing variant groups. |
| packages/contentstack-variants/src/import/variant-entries.ts | Downgrades missing data-for-variant-entry.json message from warn to debug and clarifies “no variant entries to import”. |
| packages/contentstack-import/src/import/modules/entries.ts | Removes early return on missing environments so variant entry data file creation always occurs. |
| packages/contentstack-import/test/unit/import/modules/entries.test.ts | Updates expectations to ensure publish path is exercised and variant-entry data file creation always runs. |
| packages/contentstack-variants/test/unit/import/experiences.test.ts | Adds unit/integration tests for importExperienceVersions and pending validation list behavior. |
| packages/contentstack-variants/test/unit/mock/contents/personalize/experiences/experiences.json | Adds experience fixtures used by tests. |
| packages/contentstack-variants/test/unit/mock/contents/personalize/experiences/experiences-content-types.json | Adds content-type mapping fixtures for CT attachment tests. |
| packages/contentstack-variants/test/unit/mock/contents/personalize/experiences/versions/*.json | Adds version fixtures for valid/mixed/lytics-only/empty/draft scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context); | ||
| } else if (versionReqObj?.status && !(versionReqObj.variants?.length ?? 0)) { | ||
| log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context); | ||
| log.warn(`Skipping version ${versionReqObj.status}: no valid variants after audience mapping — variants may have had no audiences or all audiences were unmapped`, this.config.context); |
- experiences.ts: pass versionReqObj (already audience-mapped) to lookUpEvents instead of original version — was relying on mutation side effect of lookUpAudiences to make this work - entries.ts: skip publish loop and success log entirely when envs is empty, instead of running the loop as a no-op; createEntryDataForVariantEntry still runs unconditionally afterward Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
Consider reviewing these vulnerabilities when fixes become available. |
…X-9469] If versionReqObj.status was e.g. PAUSED (not PAUSE), the previous code silently added a 4th key to the Record-typed map, causing Object.values to find a non-undefined entry and return true — even though handleVersionUpdateOrCreate only destructures ACTIVE/DRAFT/PAUSE and would have processed nothing. The experience UID then incorrectly entered the polling pending list, reintroducing the timeout bug. Fix: narrow versionMap to a typed object with only the three known keys and validate status against a HANDLED_STATUSES set before writing. Unrecognized statuses are logged and skipped. Also aligns the variant-skip warning message with the generic wording adopted in fix/DX-9469-v2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔒 Security Scan Results
⏱️ SLA Breach Summary
ℹ️ Vulnerabilities Without Available Fixes (Informational Only)The following vulnerabilities were detected but do not have fixes available (no upgrade or patch). These are excluded from failure thresholds:
Consider reviewing these vulnerabilities when fixes become available. |
| log.debug(`Checking for variant entry data file: ${filePath}`, this.config.context); | ||
| if (!existsSync(filePath)) { | ||
| log.warn(`Variant entry data file not found at path: ${filePath}`, this.config.context); | ||
| log.debug(`No variant entries to import (data-for-variant-entry.json not found at: ${filePath})`, this.config.context); | ||
| return; |
| // Publishing entries | ||
| if (!this.importConfig.skipEntriesPublish) { | ||
| log.info('Starting entry publishing process...', this.importConfig.context); | ||
| this.envs = fileHelper.readFileSync(this.envPath) || {}; | ||
| if (Object.keys(this.envs).length === 0) { | ||
| log.warn( | ||
| `No environments file found at ${this.envPath}. Entries will not be published.`, | ||
| this.importConfig.context, | ||
| ); | ||
| return; | ||
| } else { | ||
| log.debug(`Loaded ${Object.keys(this.envs).length} environments.`, this.importConfig.context); | ||
| } | ||
|
|
||
| for (let entryRequestOption of entryRequestOptions) { | ||
| await this.publishEntries(entryRequestOption).catch((error) => { | ||
| handleAndLogError( | ||
| error, | ||
| { ...this.importConfig.context, cTUid: entryRequestOption.cTUid, locale: entryRequestOption.locale }, | ||
| `Error in publishing entries of ${entryRequestOption.cTUid} in locale ${entryRequestOption.locale}`, | ||
| ); | ||
| }); | ||
| for (let entryRequestOption of entryRequestOptions) { | ||
| await this.publishEntries(entryRequestOption).catch((error) => { | ||
| handleAndLogError( | ||
| error, | ||
| { ...this.importConfig.context, cTUid: entryRequestOption.cTUid, locale: entryRequestOption.locale }, | ||
| `Error in publishing entries of ${entryRequestOption.cTUid} in locale ${entryRequestOption.locale}`, | ||
| ); | ||
| }); | ||
| } | ||
| log.success('All the entries have been published successfully', this.importConfig.context); | ||
| } | ||
| log.success('All the entries have been published successfully', this.importConfig.context); | ||
| } else { |
Two related bugs in the Personalize import pipeline:
experiences.ts — pendingVariantAndVariantGrpForExperience was populated with ALL created experience UIDs regardless of whether valid variants were submitted to the API. Experiences with variants:[] never get variant groups from the Personalize backend, so the validation loop timed out and set importData=false, skipping attachCTsInExperience for every experience including valid ones. Fixed by changing importExperienceVersions to return boolean and only adding UIDs to the pending set when valid variants were actually submitted. Also adds a null guard in attachCTsInExperience before accessing variantGroup to prevent a silent crash.
entries.ts — a bare return inside the no-environments-file branch of the entries import function exited the entire function, skipping createEntryDataForVariantEntry(). This meant data-for-variant-entry.json was never written whenever a stack had no environments, causing the variant-entries module to warn about the missing file and skip all variant entry imports. Fixed by removing the return.
Problems
Bug 1 — CT linking broken when any experience has empty audiences
When importing a stack that contains experiences with
referredAudiences: [](ABtests or segmented experiences with no audiences, or with only Lytics audiences),
the CLI fails to link any variant group to any content type — even for other
experiences in the same import that do have valid audiences.
Steps to reproduce:
referredAudiencesBug 2 — Variant entry data file never written when no environments exist
When the imported stack has no environments, the entries import function hits an
early
returnbeforecreateEntryDataForVariantEntry()runs. The filemapper/entries/data-for-variant-entry.jsonis never created, so thevariant-entries module warns
"Variant entry data file not found"and skips allvariant entry imports silently.
Root Causes
Bug 1:
pendingVariantAndVariantGrpForExperiencewas built from the fullexperiencesUidMapper— all created experience UIDs regardless of whether validvariants were submitted. Experiences with
variants: []never trigger variant-groupcreation in the Personalize backend. The validation polling loop exhausted all 6
retries (60 s) waiting for groups that would never appear, then set
personalizeConfig.importData = false, gatingattachCTsInExperiencefor theentire import.
Bug 2: Inside
if (!skipEntriesPublish), whenthis.envswas empty, a barereturnexited the whole import function.createEntryDataForVariantEntry()atthe end of the function was never reached.
Fixes
contentstack-variants/src/import/experiences.tsimportExperienceVersionsreturn typePromise<void>→Promise<boolean>false: no versions file,variants: [], or all variants stripped bylookUpAudiencestrue: at least one valid version was submitted to the Personalize APICaller uses the return value —
Set<string> experienceUidsWithVariantscollects UIDs only when
importExperienceVersionsreturnstrue.pendingVariantAndVariantGrpForExperience = Array.from(experienceUidsWithVariants)Null guard in
attachCTsInExperience—getVariantGroupcan return emptyvariant_groups: [], makingvariantGroupundefined. Added guard beforeaccessing it to log a warn and skip instead of crashing silently inside
Promise.allSettled.contentstack-import/src/import/modules/entries.tsRemoved the bare
returnfrom the no-environments-found branch. The publish loopdegrades gracefully with empty envs (
serializePublishEntriesnulls out all entrieswhen no environment matches), and
createEntryDataForVariantEntry()now always runs.Edge Cases Confirmed Safe
immediately →
attachCTsInExperiencecalled → null guard handles missing variant groupsFiles Changed
contentstack-variants/src/import/experiences.tscontentstack-import/src/import/modules/entries.tscontentstack-variants/test/unit/import/experiences.test.tscontentstack-variants/test/unit/mock/contents/personalize/experiences/experiences.jsoncontentstack-variants/test/unit/mock/contents/personalize/experiences/versions/*.jsoncontentstack-variants/test/unit/mock/contents/mapper/personalize/audiences/uid-mapping.json