@contentstack/cli-variants 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/import/audiences.js +8 -3
- package/lib/import/experiences.js +5 -1
- package/lib/utils/audiences-helper.js +9 -6
- package/package.json +4 -4
- package/src/import/audiences.ts +8 -2
- package/src/import/experiences.ts +3 -1
- package/src/utils/audiences-helper.ts +5 -5
- package/test/unit/import/audiences.test.ts +118 -0
- package/test/unit/mock/contents/mapper/personalize/attributes/uid-mapping.json +1 -0
- package/test/unit/mock/contents/mapper/personalize/audiences/uid-mapping.json +1 -0
- package/test/unit/mock/contents/personalize/audiences/audiences.json +44 -0
package/lib/import/audiences.js
CHANGED
|
@@ -37,7 +37,7 @@ class Audiences extends utils_1.PersonalizationAdapter {
|
|
|
37
37
|
*/
|
|
38
38
|
import() {
|
|
39
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
-
var _a, _b;
|
|
40
|
+
var _a, _b, _c;
|
|
41
41
|
yield this.init();
|
|
42
42
|
yield utils_1.fsUtil.makeDirectory(this.audienceMapperDirPath);
|
|
43
43
|
cli_utilities_1.log.debug(`Created mapper directory: ${this.audienceMapperDirPath}`, this.config.context);
|
|
@@ -53,9 +53,14 @@ class Audiences extends utils_1.PersonalizationAdapter {
|
|
|
53
53
|
for (const audience of audiences) {
|
|
54
54
|
let { name, definition, description, uid } = audience;
|
|
55
55
|
cli_utilities_1.log.debug(`Processing audience: ${name} (${uid})`, this.config.context);
|
|
56
|
+
// Skip Lytics audiences - they cannot be created via API (synced from Lytics)
|
|
57
|
+
if (((_a = audience.source) === null || _a === void 0 ? void 0 : _a.toUpperCase()) === 'LYTICS') {
|
|
58
|
+
cli_utilities_1.log.debug(`Skipping Lytics audience: ${name} (${uid})`, this.config.context);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
56
61
|
try {
|
|
57
62
|
//check whether reference attributes exists or not
|
|
58
|
-
if ((
|
|
63
|
+
if ((_b = definition === null || definition === void 0 ? void 0 : definition.rules) === null || _b === void 0 ? void 0 : _b.length) {
|
|
59
64
|
cli_utilities_1.log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
|
|
60
65
|
definition.rules = (0, utils_1.lookUpAttributes)(definition.rules, attributesUid);
|
|
61
66
|
cli_utilities_1.log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
|
|
@@ -67,7 +72,7 @@ class Audiences extends utils_1.PersonalizationAdapter {
|
|
|
67
72
|
const audienceRes = yield this.createAudience({ definition, name, description });
|
|
68
73
|
//map old audience uid to new audience uid
|
|
69
74
|
//mapper file is used to check whether audience created or not before creating experience
|
|
70
|
-
this.audiencesUidMapper[uid] = (
|
|
75
|
+
this.audiencesUidMapper[uid] = (_c = audienceRes === null || audienceRes === void 0 ? void 0 : audienceRes.uid) !== null && _c !== void 0 ? _c : '';
|
|
71
76
|
cli_utilities_1.log.debug(`Created audience: ${uid} -> ${audienceRes === null || audienceRes === void 0 ? void 0 : audienceRes.uid}`, this.config.context);
|
|
72
77
|
}
|
|
73
78
|
catch (error) {
|
|
@@ -156,12 +156,16 @@ class Experiences extends utils_1.PersonalizationAdapter {
|
|
|
156
156
|
};
|
|
157
157
|
// Process each version and map them by status
|
|
158
158
|
versions.forEach((version) => {
|
|
159
|
+
var _a, _b, _c, _d;
|
|
159
160
|
let versionReqObj = (0, utils_1.lookUpAudiences)(version, this.audiencesUid);
|
|
160
161
|
versionReqObj = (0, utils_1.lookUpEvents)(version, this.eventsUid);
|
|
161
|
-
if (versionReqObj && versionReqObj.status) {
|
|
162
|
+
if (versionReqObj && versionReqObj.status && ((_b = (_a = versionReqObj.variants) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) {
|
|
162
163
|
versionMap[versionReqObj.status] = versionReqObj;
|
|
163
164
|
cli_utilities_1.log.debug(`Mapped version with status: ${versionReqObj.status}`, this.config.context);
|
|
164
165
|
}
|
|
166
|
+
else if ((versionReqObj === null || versionReqObj === void 0 ? void 0 : versionReqObj.status) && !((_d = (_c = versionReqObj.variants) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0)) {
|
|
167
|
+
cli_utilities_1.log.warn(`Skipping version ${versionReqObj.status}: no valid variants (all had unmapped Lytics audiences)`, this.config.context);
|
|
168
|
+
}
|
|
165
169
|
});
|
|
166
170
|
// Prioritize updating or creating versions based on the order: ACTIVE -> DRAFT -> PAUSE
|
|
167
171
|
return yield this.handleVersionUpdateOrCreate(experience, versionMap);
|
|
@@ -30,7 +30,7 @@ function updateAudiences(audiences, audiencesUid) {
|
|
|
30
30
|
* @returns
|
|
31
31
|
*/
|
|
32
32
|
const lookUpAudiences = (experience, audiencesUid) => {
|
|
33
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
33
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
34
34
|
cli_utilities_1.log.debug('Starting audience lookup for experience');
|
|
35
35
|
cli_utilities_1.log.debug(`Available audience mappings: ${(_a = Object.keys(audiencesUid)) === null || _a === void 0 ? void 0 : _a.length}`);
|
|
36
36
|
// Update experience variations
|
|
@@ -57,10 +57,13 @@ const lookUpAudiences = (experience, audiencesUid) => {
|
|
|
57
57
|
for (let index = experience.variants.length - 1; index >= 0; index--) {
|
|
58
58
|
const expVariations = experience.variants[index];
|
|
59
59
|
cli_utilities_1.log.debug(`Processing variant ${index + 1}/${experience.variants.length} of type: ${expVariations['__type']}`);
|
|
60
|
-
if (expVariations['__type'] === 'SegmentedVariant' && ((_d = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _d === void 0 ? void 0 : _d.length)) {
|
|
61
|
-
cli_utilities_1.log.debug(`Found ${expVariations.audiences.length} audiences in SegmentedVariant`);
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (expVariations['__type'] === 'SegmentedVariant' && (((_d = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _d === void 0 ? void 0 : _d.length) || ((_e = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _e === void 0 ? void 0 : _e.length))) {
|
|
61
|
+
cli_utilities_1.log.debug(`Found ${(_g = (_f = expVariations.audiences) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0} audiences in SegmentedVariant`);
|
|
62
|
+
if ((_h = expVariations === null || expVariations === void 0 ? void 0 : expVariations.audiences) === null || _h === void 0 ? void 0 : _h.length)
|
|
63
|
+
updateAudiences(expVariations.audiences, audiencesUid);
|
|
64
|
+
if ((_j = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _j === void 0 ? void 0 : _j.length)
|
|
65
|
+
updateAudiences(expVariations.lyticsAudiences, audiencesUid);
|
|
66
|
+
if (!(((_k = expVariations.audiences) === null || _k === void 0 ? void 0 : _k.length) || ((_l = expVariations === null || expVariations === void 0 ? void 0 : expVariations.lyticsAudiences) === null || _l === void 0 ? void 0 : _l.length))) {
|
|
64
67
|
cli_utilities_1.log.warn('No audiences remaining after mapping. Removing variant.');
|
|
65
68
|
experience.variants.splice(index, 1);
|
|
66
69
|
}
|
|
@@ -73,7 +76,7 @@ const lookUpAudiences = (experience, audiencesUid) => {
|
|
|
73
76
|
else {
|
|
74
77
|
cli_utilities_1.log.debug('No variations or variants found in experience');
|
|
75
78
|
}
|
|
76
|
-
if (((
|
|
79
|
+
if (((_m = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _m === void 0 ? void 0 : _m.hasOwnProperty('audience')) && ((_q = (_p = (_o = experience === null || experience === void 0 ? void 0 : experience.targeting) === null || _o === void 0 ? void 0 : _o.audience) === null || _p === void 0 ? void 0 : _p.audiences) === null || _q === void 0 ? void 0 : _q.length)) {
|
|
77
80
|
cli_utilities_1.log.debug(`Processing ${experience.targeting.audience.audiences.length} targeting audiences`);
|
|
78
81
|
// Update targeting audiences
|
|
79
82
|
updateAudiences(experience.targeting.audience.audiences, audiencesUid);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentstack/cli-variants",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"description": "Variants plugin",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"typescript": "^5.8.3"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@contentstack/cli-utilities": "~1.
|
|
30
|
+
"@contentstack/cli-utilities": "~1.17.4",
|
|
31
31
|
"@oclif/core": "^4.3.0",
|
|
32
32
|
"@oclif/plugin-help": "^6.2.28",
|
|
33
|
-
"lodash": "^4.17.
|
|
33
|
+
"lodash": "^4.17.23",
|
|
34
34
|
"mkdirp": "^1.0.4",
|
|
35
35
|
"winston": "^3.17.0"
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
package/src/import/audiences.ts
CHANGED
|
@@ -70,10 +70,16 @@ export default class Audiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
70
70
|
for (const audience of audiences) {
|
|
71
71
|
let { name, definition, description, uid } = audience;
|
|
72
72
|
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
|
+
|
|
74
80
|
try {
|
|
75
81
|
//check whether reference attributes exists or not
|
|
76
|
-
if (definition
|
|
82
|
+
if (definition?.rules?.length) {
|
|
77
83
|
log.debug(`Processing ${definition.rules.length} definition rules for audience: ${name}`, this.config.context);
|
|
78
84
|
definition.rules = lookUpAttributes(definition.rules, attributesUid);
|
|
79
85
|
log.debug(`Processed definition rules, remaining rules: ${definition.rules.length}`, this.config.context);
|
|
@@ -203,9 +203,11 @@ export default class Experiences extends PersonalizationAdapter<ImportConfig> {
|
|
|
203
203
|
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
|
|
204
204
|
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;
|
|
205
205
|
|
|
206
|
-
if (versionReqObj && versionReqObj.status) {
|
|
206
|
+
if (versionReqObj && versionReqObj.status && (versionReqObj.variants?.length ?? 0) > 0) {
|
|
207
207
|
versionMap[versionReqObj.status] = versionReqObj;
|
|
208
208
|
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);
|
|
209
211
|
}
|
|
210
212
|
});
|
|
211
213
|
|
|
@@ -65,11 +65,11 @@ export const lookUpAudiences = (
|
|
|
65
65
|
const expVariations = experience.variants[index];
|
|
66
66
|
log.debug(`Processing variant ${index + 1}/${experience.variants.length} of type: ${expVariations['__type']}`);
|
|
67
67
|
|
|
68
|
-
if (expVariations['__type'] === 'SegmentedVariant' && expVariations?.audiences?.length) {
|
|
69
|
-
log.debug(`Found ${expVariations.audiences
|
|
70
|
-
updateAudiences(expVariations.audiences, audiencesUid);
|
|
71
|
-
|
|
72
|
-
if (!expVariations.audiences
|
|
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)) {
|
|
73
73
|
log.warn('No audiences remaining after mapping. Removing variant.');
|
|
74
74
|
experience.variants.splice(index, 1);
|
|
75
75
|
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"contentstack-audience-001":"new-contentstack-uid","no-source-audience-001":"new-contentstack-uid"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"uid": "contentstack-audience-001",
|
|
4
|
+
"name": "Contentstack Test Audience",
|
|
5
|
+
"description": "Audience with rules",
|
|
6
|
+
"definition": {
|
|
7
|
+
"__type": "RuleCombination",
|
|
8
|
+
"combinationType": "AND",
|
|
9
|
+
"rules": [
|
|
10
|
+
{
|
|
11
|
+
"__type": "Rule",
|
|
12
|
+
"attribute": { "__type": "PresetAttributeReference", "ref": "DEVICE_TYPE" },
|
|
13
|
+
"attributeMatchOptions": { "__type": "StringMatchOptions", "value": "MOBILE" },
|
|
14
|
+
"attributeMatchCondition": "STRING_EQUALS",
|
|
15
|
+
"invertCondition": false
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"uid": "lytics-audience-001",
|
|
22
|
+
"name": "Lytics Audience",
|
|
23
|
+
"description": "From Lytics",
|
|
24
|
+
"slug": "lytics_audience",
|
|
25
|
+
"source": "LYTICS"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"uid": "lytics-lowercase-001",
|
|
29
|
+
"name": "Lytics Lowercase",
|
|
30
|
+
"description": "source is lowercase",
|
|
31
|
+
"slug": "lytics_lowercase",
|
|
32
|
+
"source": "lytics"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"uid": "no-source-audience-001",
|
|
36
|
+
"name": "No Source Audience",
|
|
37
|
+
"description": "Audience without source field",
|
|
38
|
+
"definition": {
|
|
39
|
+
"__type": "RuleCombination",
|
|
40
|
+
"combinationType": "AND",
|
|
41
|
+
"rules": []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|