-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathexperiences.ts
More file actions
379 lines (338 loc) · 17.7 KB
/
experiences.ts
File metadata and controls
379 lines (338 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import { join, resolve } from 'path';
import { existsSync } from 'fs';
import values from 'lodash/values';
import cloneDeep from 'lodash/cloneDeep';
import { sanitizePath, log, handleAndLogError } from '@contentstack/cli-utilities';
import { PersonalizationAdapter, fsUtil, lookUpAudiences, lookUpEvents } from '../utils';
import {
APIConfig,
ImportConfig,
ExperienceStruct,
CreateExperienceInput,
CreateExperienceVersionInput,
} from '../types';
export default class Experiences extends PersonalizationAdapter<ImportConfig> {
private createdCTs: string[];
private mapperDirPath: string;
private cmsVariantPath: string;
private cTsSuccessPath: string;
private failedCmsExpPath: string;
private expMapperDirPath: string;
private eventsMapperPath: string;
private experiencesPath: string;
private experiencesDirPath: string;
private audiencesMapperPath: string;
private cmsVariantGroupPath: string;
private experienceVariantsIdsPath: string;
private variantUidMapperFilePath: string;
private expThresholdTimer: number;
private maxValidateRetry: number;
private experiencesUidMapperPath: string;
private experienceCTsPath: string;
private expCheckIntervalDuration: number;
private cmsVariants: Record<string, Record<string, string>>;
private cmsVariantGroups: Record<string, unknown>;
private experiencesUidMapper: Record<string, string>;
private pendingVariantAndVariantGrpForExperience: string[];
private audiencesUid: Record<string, string>;
private eventsUid: Record<string, string>;
private personalizeConfig: ImportConfig['modules']['personalize'];
private audienceConfig: ImportConfig['modules']['personalize']['audiences'];
private experienceConfig: ImportConfig['modules']['personalize']['experiences'];
constructor(public readonly config: ImportConfig) {
const conf: APIConfig = {
config,
baseURL: config.modules.personalize.baseURL[config.region.name],
headers: { 'X-Project-Uid': config.modules.personalize.project_id },
cmaConfig: {
baseURL: config.region.cma + `/v3`,
headers: { api_key: config.apiKey },
},
};
super(Object.assign(config, conf));
this.personalizeConfig = this.config.modules.personalize;
this.experiencesDirPath = resolve(
sanitizePath(this.config.data),
sanitizePath(this.personalizeConfig.dirName),
sanitizePath(this.personalizeConfig.experiences.dirName),
);
this.experiencesPath = join(
sanitizePath(this.experiencesDirPath),
sanitizePath(this.personalizeConfig.experiences.fileName),
);
this.experienceConfig = this.personalizeConfig.experiences;
this.audienceConfig = this.personalizeConfig.audiences;
this.mapperDirPath = resolve(
sanitizePath(this.config.backupDir),
'mapper',
sanitizePath(this.personalizeConfig.dirName),
);
this.expMapperDirPath = resolve(sanitizePath(this.mapperDirPath), sanitizePath(this.experienceConfig.dirName));
this.experiencesUidMapperPath = resolve(sanitizePath(this.expMapperDirPath), 'uid-mapping.json');
this.cmsVariantGroupPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variant-groups.json');
this.cmsVariantPath = resolve(sanitizePath(this.expMapperDirPath), 'cms-variants.json');
this.audiencesMapperPath = resolve(
sanitizePath(this.mapperDirPath),
sanitizePath(this.audienceConfig.dirName),
'uid-mapping.json',
);
this.eventsMapperPath = resolve(sanitizePath(this.mapperDirPath), 'events', 'uid-mapping.json');
this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
this.failedCmsExpPath = resolve(sanitizePath(this.expMapperDirPath), 'failed-cms-experience.json');
this.experienceCTsPath = resolve(sanitizePath(this.experiencesDirPath), 'experiences-content-types.json');
this.experienceVariantsIdsPath = resolve(
sanitizePath(this.config.data),
sanitizePath(this.personalizeConfig.dirName),
sanitizePath(this.experienceConfig.dirName),
'experiences-variants-ids.json',
);
this.variantUidMapperFilePath = resolve(sanitizePath(this.expMapperDirPath), 'variants-uid-mapping.json');
this.experiencesUidMapper = {};
this.cmsVariantGroups = {};
this.cmsVariants = {};
this.expThresholdTimer = this.experienceConfig?.thresholdTimer ?? 30000;
this.expCheckIntervalDuration = this.experienceConfig?.checkIntervalDuration ?? 5000;
this.maxValidateRetry = Math.round(this.expThresholdTimer / this.expCheckIntervalDuration);
this.pendingVariantAndVariantGrpForExperience = [];
this.cTsSuccessPath = resolve(sanitizePath(this.config.backupDir), 'mapper', 'content_types', 'success.json');
this.createdCTs = [];
this.audiencesUid = (fsUtil.readFile(this.audiencesMapperPath, true) as Record<string, string>) || {};
this.eventsUid = (fsUtil.readFile(this.eventsMapperPath, true) as Record<string, string>) || {};
this.config.context.module = 'experiences';
}
/**
* The function asynchronously imports experiences from a JSON file and creates them in the system.
*/
async import() {
await this.init();
await fsUtil.makeDirectory(this.expMapperDirPath);
log.debug(`Created mapper directory: ${this.expMapperDirPath}`, this.config.context);
if (existsSync(this.experiencesPath)) {
log.debug(`Loading experiences from: ${this.experiencesPath}`, this.config.context);
try {
const experiences = fsUtil.readFile(this.experiencesPath, true) as ExperienceStruct[];
log.info(`Found ${experiences.length} experiences to import`, this.config.context);
for (const experience of experiences) {
const { uid, ...restExperienceData } = experience;
log.debug(`Processing experience: ${uid}`, this.config.context);
//check whether reference audience exists or not that referenced in variations having __type equal to AudienceBasedVariation & targeting
let experienceReqObj: CreateExperienceInput = lookUpAudiences(restExperienceData, this.audiencesUid);
//check whether events exists or not that referenced in metrics
experienceReqObj = lookUpEvents(experienceReqObj, this.eventsUid);
const expRes = (await this.createExperience(experienceReqObj)) as ExperienceStruct;
//map old experience uid to new experience uid
this.experiencesUidMapper[uid] = expRes?.uid ?? '';
log.debug(`Created experience: ${uid} -> ${expRes?.uid}`, this.config.context);
try {
// import versions of experience
await this.importExperienceVersions(expRes, uid);
} catch (error) {
handleAndLogError(error, this.config.context, `Failed to import experience versions for ${expRes.uid}`);
}
}
fsUtil.writeFile(this.experiencesUidMapperPath, this.experiencesUidMapper);
log.success('Experiences created successfully', this.config.context);
log.info('Validating variant and variant group creation',this.config.context);
this.pendingVariantAndVariantGrpForExperience = values(cloneDeep(this.experiencesUidMapper));
const jobRes = await this.validateVariantGroupAndVariantsCreated();
fsUtil.writeFile(this.cmsVariantPath, this.cmsVariants);
fsUtil.writeFile(this.cmsVariantGroupPath, this.cmsVariantGroups);
if (jobRes) {
log.success('Variant and variant groups created successfully', this.config.context);
} else {
log.error('Failed to create variants and variant groups', this.config.context);
this.personalizeConfig.importData = false;
}
if (this.personalizeConfig.importData) {
log.info('Attaching content types to experiences', this.config.context);
await this.attachCTsInExperience();
log.success('Content types attached to experiences successfully', this.config.context);
}
await this.createVariantIdMapper();
} catch (error) {
handleAndLogError(error, this.config.context);
}
} else {
log.warn(`Experiences file not found: ${this.experiencesPath}`, this.config.context);
}
}
/**
* function import experience versions from a JSON file and creates them in the project.
*/
async importExperienceVersions(experience: ExperienceStruct, oldExperienceUid: string) {
log.debug(`Importing versions for experience: ${oldExperienceUid}`, this.config.context);
const versionsPath = resolve(
sanitizePath(this.experiencesDirPath),
'versions',
`${sanitizePath(oldExperienceUid)}.json`,
);
if (!existsSync(versionsPath)) {
log.debug(`No versions file found for experience: ${oldExperienceUid}`, this.config.context);
return;
}
const versions = fsUtil.readFile(versionsPath, true) as ExperienceStruct[];
log.debug(`Found ${versions.length} versions for experience: ${oldExperienceUid}`, this.config.context);
const versionMap: Record<string, CreateExperienceVersionInput | undefined> = {
ACTIVE: undefined,
DRAFT: undefined,
PAUSE: undefined,
};
// Process each version and map them by status
versions.forEach((version) => {
let versionReqObj = lookUpAudiences(version, this.audiencesUid) as CreateExperienceVersionInput;
versionReqObj = lookUpEvents(version, this.eventsUid) as CreateExperienceVersionInput;
if (versionReqObj && versionReqObj.status && (versionReqObj.variants?.length ?? 0) > 0) {
versionMap[versionReqObj.status] = versionReqObj;
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);
}
});
// Prioritize updating or creating versions based on the order: ACTIVE -> DRAFT -> PAUSE
return await this.handleVersionUpdateOrCreate(experience, versionMap);
}
// Helper method to handle version update or creation logic
private async handleVersionUpdateOrCreate(
experience: ExperienceStruct,
versionMap: Record<string, CreateExperienceVersionInput | undefined>,
) {
log.debug(`Handling version update/create for experience: ${experience.uid}`, this.config.context);
const { ACTIVE, DRAFT, PAUSE } = versionMap;
let latestVersionUsed = false;
if (ACTIVE) {
log.debug(`Updating experience version to ACTIVE for: ${experience.uid}`, this.config.context);
await this.updateExperienceVersion(experience.uid, experience.latestVersion, ACTIVE);
latestVersionUsed = true;
}
if (DRAFT) {
if (latestVersionUsed) {
log.debug(`Creating new DRAFT version for: ${experience.uid}`, this.config.context);
await this.createExperienceVersion(experience.uid, DRAFT);
} else {
log.debug(`Updating experience version to DRAFT for: ${experience.uid}`, this.config.context);
await this.updateExperienceVersion(experience.uid, experience.latestVersion, DRAFT);
latestVersionUsed = true;
}
}
if (PAUSE) {
if (latestVersionUsed) {
log.debug(`Creating new PAUSED version for: ${experience.uid}`, this.config.context);
await this.createExperienceVersion(experience.uid, PAUSE);
} else {
log.debug(`Updating experience version to PAUSED for: ${experience.uid}`, this.config.context);
await this.updateExperienceVersion(experience.uid, experience.latestVersion, PAUSE);
}
}
}
/**
* function to validate if all variant groups and variants have been created using personalize background job
* store the variant groups data in mapper/personalize/experiences/cms-variant-groups.json and the variants data
* in mapper/personalize/experiences/cms-variants.json. If not, invoke validateVariantGroupAndVariantsCreated after some delay.
* @param retryCount Counter to track the number of times the function has been called
* @returns
*/
async validateVariantGroupAndVariantsCreated(retryCount = 0): Promise<any> {
log.debug(`Validating variant groups and variants creation - attempt ${retryCount + 1}/${this.maxValidateRetry}`, this.config.context);
try {
const promises = this.pendingVariantAndVariantGrpForExperience.map(async (expUid) => {
log.debug(`Checking experience: ${expUid}`, this.config.context);
const expRes = await this.getExperience(expUid);
const variants = expRes?._cms?.variants ?? {};
if (expRes?._cms && expRes?._cms?.variantGroup && Object.keys(variants).length > 0) {
log.debug(`Found variants and variant group for experience: ${expUid}`, this.config.context);
this.cmsVariants[expUid] = expRes._cms?.variants ?? {};
this.cmsVariantGroups[expUid] = expRes._cms?.variantGroup ?? {};
return expUid; // Return the expUid for filtering later
} else {
log.debug(`Variants or variant group not ready for experience: ${expUid}`, this.config.context);
}
});
await Promise.all(promises);
retryCount++;
if (this.pendingVariantAndVariantGrpForExperience?.length) {
if (retryCount !== this.maxValidateRetry) {
log.debug(`Waiting ${this.expCheckIntervalDuration}ms before retry`, this.config.context);
await this.delay(this.expCheckIntervalDuration);
// Filter out the processed elements
this.pendingVariantAndVariantGrpForExperience = this.pendingVariantAndVariantGrpForExperience.filter(
(uid) => !this.cmsVariants[uid],
);
return this.validateVariantGroupAndVariantsCreated(retryCount);
} else {
log.error('Personalize job failed to create variants and variant groups.', this.config.context);
log.error(`Failed experiences: ${this.pendingVariantAndVariantGrpForExperience.join(', ')}`, this.config.context);
fsUtil.writeFile(this.failedCmsExpPath, this.pendingVariantAndVariantGrpForExperience);
return false;
}
} else {
log.debug('All variant groups and variants created successfully', this.config.context);
return true;
}
} catch (error) {
handleAndLogError(error, this.config.context);
throw error;
}
}
async attachCTsInExperience() {
log.debug('Attaching content types to experiences', this.config.context);
try {
// Read the created content types from the file
this.createdCTs = fsUtil.readFile(this.cTsSuccessPath, true) as any;
if (!this.createdCTs) {
log.warn('No content types created.', this.config.context);
return;
}
log.debug(`Found ${this.createdCTs.length} created content types`, this.config.context);
const experienceCTsMap = fsUtil.readFile(this.experienceCTsPath, true) as Record<string, string[]>;
return await Promise.allSettled(
Object.entries(this.experiencesUidMapper).map(async ([oldExpUid, newExpUid]) => {
if (experienceCTsMap[oldExpUid]?.length) {
log.debug(`Processing content types for experience: ${oldExpUid} -> ${newExpUid}`, this.config.context);
// Filter content types that were created
const updatedContentTypes = experienceCTsMap[oldExpUid].filter(
(ct: any) => this.createdCTs.includes(ct?.uid) && ct.status === 'linked',
);
if (updatedContentTypes?.length) {
log.debug(`Attaching ${updatedContentTypes.length} content types to experience: ${newExpUid}`, this.config.context);
const { variant_groups: [variantGroup] = [] } =
(await this.getVariantGroup({ experienceUid: newExpUid })) || {};
variantGroup.content_types = updatedContentTypes;
// Update content types detail in the new experience asynchronously
return await this.updateVariantGroup(variantGroup);
} else {
log.debug(`No valid content types found for experience: ${newExpUid}`, this.config.context);
}
} else {
log.debug(`No content types mapped for experience: ${oldExpUid}`, this.config.context);
}
}),
);
} catch (error) {
handleAndLogError(error, this.config.context, 'Failed to attach content type with experience');
}
}
async createVariantIdMapper() {
log.debug('Creating variant ID mapper', this.config.context);
try {
const experienceVariantIds: any = fsUtil.readFile(this.experienceVariantsIdsPath, true) || [];
log.debug(`Found ${experienceVariantIds.length} experience variant IDs to process`, this.config.context);
const variantUIDMapper: Record<string, string> = {};
for (let experienceVariantId of experienceVariantIds) {
const [experienceId, variantShortId, oldVariantId] = experienceVariantId.split('-');
const latestVariantId = this.cmsVariants[this.experiencesUidMapper[experienceId]]?.[variantShortId];
if (latestVariantId) {
variantUIDMapper[oldVariantId] = latestVariantId;
log.debug(`Mapped variant ID: ${oldVariantId} -> ${latestVariantId}`, this.config.context);
} else {
log.warn(`Could not find variant ID mapping for: ${experienceVariantId}`, this.config.context);
}
}
log.debug(`Created ${Object.keys(variantUIDMapper).length} variant ID mappings`, this.config.context);
fsUtil.writeFile(this.variantUidMapperFilePath, variantUIDMapper);
log.debug(`Variant ID mapper saved to: ${this.variantUidMapperFilePath}`, this.config.context);
} catch (error) {
handleAndLogError(error, this.config.context, 'Failed to create variant ID mapper');
throw error;
}
}
}