Skip to content

Commit cbfec0a

Browse files
authored
Merge pull request #2427 from contentstack/fix/DX-3445
DX - 3445 Experience import errors when variants reference Lytics audiences
2 parents aa08882 + db01c8a commit cbfec0a

File tree

14 files changed

+1477
-1415
lines changed

14 files changed

+1477
-1415
lines changed

.talismanrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
fileignoreconfig:
22
- filename: package-lock.json
3-
checksum: 1b011574c5a640f7132f2dcabfced269cb497ddd3270524ec32abe3cb4a95cb5
3+
checksum: c91b9e5fba1c84c0b6de15ad2f8cce698a5c781c9db31bebb7a3ad63ee88d9e1
44
- filename: pnpm-lock.yaml
5-
checksum: 91ffcd3364bcbef7dad0d25547849a572dc9ebd004999c3ede85c7730959a2e5
5+
checksum: 8405d813bbcc584a7540542acfdbc27f5b8768da60354b7eff9f6cd93c3d832d
66
- filename: packages/contentstack-bootstrap/src/bootstrap/utils.ts
77
checksum: 6e6fb00bb11b03141e5ad27eeaa4af9718dc30520c3e73970bc208cc0ba2a7d2
88
version: '1.0'

package-lock.json

Lines changed: 575 additions & 674 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contentstack-export/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dependencies": {
88
"@contentstack/cli-command": "~1.7.2",
99
"@oclif/core": "^4.3.3",
10-
"@contentstack/cli-variants": "~1.3.7",
10+
"@contentstack/cli-variants": "~1.3.8",
1111
"@contentstack/cli-utilities": "~1.17.4",
1212
"async": "^3.2.6",
1313
"big-json": "^3.2.0",

packages/contentstack-import/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@contentstack/cli-audit": "~1.18.0",
99
"@contentstack/cli-command": "~1.7.2",
1010
"@contentstack/cli-utilities": "~1.17.4",
11-
"@contentstack/cli-variants": "~1.3.7",
11+
"@contentstack/cli-variants": "~1.3.8",
1212
"@oclif/core": "^4.3.0",
1313
"big-json": "^3.2.0",
1414
"bluebird": "^3.7.2",

packages/contentstack-variants/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/cli-variants",
3-
"version": "1.3.7",
3+
"version": "1.3.8",
44
"description": "Variants plugin",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

packages/contentstack-variants/src/import/audiences.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
7070
for (const audience of audiences) {
7171
let { name, definition, description, uid } = audience;
7272
log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
73-
73+
74+
// Skip Lytics audiences - they cannot be created via API (synced from Lytics)
75+
if (audience.source?.toUpperCase() === 'LYTICS') {
76+
log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
77+
continue;
78+
}
79+
7480
try {
7581
//check whether reference attributes exists or not
76-
if (definition.rules?.length) {
82+
if (definition?.rules?.length) {
7783
log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
7884
definition.rules = lookUpAttributes(definition.rules, attributesUid);
7985
log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);

packages/contentstack-variants/src/import/experiences.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
203203
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
204204
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;
205205

206-
if (versionReqObj && versionReqObj.status) {
206+
if (versionReqObj && versionReqObj.status && (versionReqObj.variants?.length ?? 0) > 0) {
207207
versionMap[versionReqObj.status] = versionReqObj;
208208
log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context);
209+
} else if (versionReqObj?.status && !(versionReqObj.variants?.length ?? 0)) {
210+
log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context);
209211
}
210212
});
211213

packages/contentstack-variants/src/utils/audiences-helper.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ export const lookUpAudiences = (
6565
const expVariations = experience.variants[index];
6666
log.debug(`Processing variant ${index + 1}/${experience.variants.length} of type: ${expVariations['__type']}`);
6767

68-
if (expVariations['__type'] === 'SegmentedVariant' && expVariations?.audiences?.length) {
69-
log.debug(`Found ${expVariations.audiences.length} audiences in SegmentedVariant`);
70-
updateAudiences(expVariations.audiences, audiencesUid);
71-
72-
if (!expVariations.audiences.length) {
68+
if (expVariations['__type'] === 'SegmentedVariant' && (expVariations?.audiences?.length || expVariations?.lyticsAudiences?.length)) {
69+
log.debug(`Found ${expVariations.audiences?.length ?? 0} audiences in SegmentedVariant`);
70+
if (expVariations?.audiences?.length) updateAudiences(expVariations.audiences, audiencesUid);
71+
if (expVariations?.lyticsAudiences?.length) updateAudiences(expVariations.lyticsAudiences, audiencesUid);
72+
if (!(expVariations.audiences?.length || expVariations?.lyticsAudiences?.length)) {
7373
log.warn('No audiences remaining after mapping. Removing variant.');
7474
experience.variants.splice(index, 1);
7575
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { expect } from '@oclif/test';
2+
import cloneDeep from 'lodash/cloneDeep';
3+
import { fancy } from '@contentstack/cli-dev-dependencies';
4+
5+
import importConf from '../mock/import-config.json';
6+
import { Import, ImportConfig } from '../../../src';
7+
8+
describe('Audiences Import', () => {
9+
let config: ImportConfig;
10+
let createAudienceCalls: Array<{ name: string }> = [];
11+
12+
const test = fancy.stdout({ print: process.env.PRINT === 'true' || false });
13+
14+
beforeEach(() => {
15+
config = cloneDeep(importConf) as unknown as ImportConfig;
16+
createAudienceCalls = [];
17+
// Audiences uses modules.personalize and region - add them for tests
18+
config.modules.personalize = {
19+
...(config.modules as any).personalization,
20+
dirName: 'personalize',
21+
baseURL: {
22+
na: 'https://personalization.na-api.contentstack.com',
23+
eu: 'https://personalization.eu-api.contentstack.com',
24+
},
25+
} as any;
26+
config.region = { name: 'eu' } as any;
27+
config.context = config.context || {};
28+
});
29+
30+
describe('import method - Lytics audience skip', () => {
31+
test
32+
.stub(Import.Audiences.prototype, 'init', async () => {})
33+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
34+
createAudienceCalls.push({ name: payload.name });
35+
return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
36+
}) as any)
37+
.it('should skip Lytics audiences and not call createAudience for them', async () => {
38+
const audiencesInstance = new Import.Audiences(config);
39+
await audiencesInstance.import();
40+
41+
const lyticsNames = createAudienceCalls.filter(
42+
(c) => c.name === 'Lytics Audience' || c.name === 'Lytics Lowercase',
43+
);
44+
expect(lyticsNames.length).to.equal(0);
45+
});
46+
47+
test
48+
.stub(Import.Audiences.prototype, 'init', async () => {})
49+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
50+
createAudienceCalls.push({ name: payload.name });
51+
return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
52+
}) as any)
53+
.it('should process audiences with undefined source', async () => {
54+
const audiencesInstance = new Import.Audiences(config);
55+
await audiencesInstance.import();
56+
57+
const noSourceCall = createAudienceCalls.find((c) => c.name === 'No Source Audience');
58+
expect(noSourceCall).to.not.be.undefined;
59+
});
60+
61+
test
62+
.stub(Import.Audiences.prototype, 'init', async () => {})
63+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
64+
createAudienceCalls.push({ name: payload.name });
65+
return { uid: `new-${payload.name.replace(/\s/g, '-')}`, name: payload.name };
66+
}) as any)
67+
.it('should skip audience with source "lytics" (lowercase)', async () => {
68+
const audiencesInstance = new Import.Audiences(config);
69+
await audiencesInstance.import();
70+
71+
const lyticsLowercaseCall = createAudienceCalls.find((c) => c.name === 'Lytics Lowercase');
72+
expect(lyticsLowercaseCall).to.be.undefined;
73+
});
74+
75+
test
76+
.stub(Import.Audiences.prototype, 'init', async () => {})
77+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
78+
createAudienceCalls.push({ name: payload.name });
79+
return { uid: `new-uid-${payload.name}`, name: payload.name };
80+
}) as any)
81+
.it('should call createAudience only for non-Lytics audiences', async () => {
82+
const audiencesInstance = new Import.Audiences(config);
83+
await audiencesInstance.import();
84+
85+
// 4 audiences in mock: 2 Lytics (skip), 2 non-Lytics (Contentstack Test, No Source)
86+
expect(createAudienceCalls.length).to.equal(2);
87+
});
88+
89+
test
90+
.stub(Import.Audiences.prototype, 'init', async () => {})
91+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
92+
createAudienceCalls.push({ name: payload.name });
93+
return { uid: 'new-contentstack-uid', name: payload.name };
94+
}) as any)
95+
.it('should not add Lytics audiences to audiencesUidMapper', async () => {
96+
const audiencesInstance = new Import.Audiences(config);
97+
await audiencesInstance.import();
98+
99+
const mapper = (audiencesInstance as any).audiencesUidMapper;
100+
expect(mapper['lytics-audience-001']).to.be.undefined;
101+
expect(mapper['lytics-lowercase-001']).to.be.undefined;
102+
});
103+
104+
test
105+
.stub(Import.Audiences.prototype, 'init', async () => {})
106+
.stub(Import.Audiences.prototype, 'createAudience', (async (payload: any) => {
107+
createAudienceCalls.push({ name: payload.name });
108+
return { uid: 'new-contentstack-uid', name: payload.name };
109+
}) as any)
110+
.it('should add Contentstack audiences to audiencesUidMapper', async () => {
111+
const audiencesInstance = new Import.Audiences(config);
112+
await audiencesInstance.import();
113+
114+
const mapper = (audiencesInstance as any).audiencesUidMapper;
115+
expect(mapper['contentstack-audience-001']).to.equal('new-contentstack-uid');
116+
});
117+
});
118+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)