strapi-plugin-magic-mail 2.2.4 → 2.2.5
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/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/AddAccountModal.jsx +0 -1943
- package/admin/src/components/Initializer.jsx +0 -14
- package/admin/src/components/LicenseGuard.jsx +0 -475
- package/admin/src/components/PluginIcon.jsx +0 -5
- package/admin/src/hooks/useAuthRefresh.js +0 -44
- package/admin/src/hooks/useLicense.js +0 -158
- package/admin/src/index.js +0 -87
- package/admin/src/pages/Analytics.jsx +0 -762
- package/admin/src/pages/App.jsx +0 -111
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
- package/admin/src/pages/HomePage.jsx +0 -1170
- package/admin/src/pages/LicensePage.jsx +0 -430
- package/admin/src/pages/RoutingRules.jsx +0 -1141
- package/admin/src/pages/Settings.jsx +0 -603
- package/admin/src/pluginId.js +0 -3
- package/admin/src/translations/de.json +0 -71
- package/admin/src/translations/en.json +0 -70
- package/admin/src/translations/es.json +0 -71
- package/admin/src/translations/fr.json +0 -71
- package/admin/src/translations/pt.json +0 -71
- package/admin/src/utils/fetchWithRetry.js +0 -123
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -157
- package/server/src/config/features.js +0 -260
- package/server/src/config/index.js +0 -9
- package/server/src/content-types/email-account/schema.json +0 -93
- package/server/src/content-types/email-event/index.js +0 -8
- package/server/src/content-types/email-event/schema.json +0 -57
- package/server/src/content-types/email-link/index.js +0 -8
- package/server/src/content-types/email-link/schema.json +0 -49
- package/server/src/content-types/email-log/index.js +0 -8
- package/server/src/content-types/email-log/schema.json +0 -106
- package/server/src/content-types/email-template/schema.json +0 -74
- package/server/src/content-types/email-template-version/schema.json +0 -60
- package/server/src/content-types/index.js +0 -33
- package/server/src/content-types/routing-rule/schema.json +0 -59
- package/server/src/controllers/accounts.js +0 -229
- package/server/src/controllers/analytics.js +0 -361
- package/server/src/controllers/controller.js +0 -26
- package/server/src/controllers/email-designer.js +0 -474
- package/server/src/controllers/index.js +0 -21
- package/server/src/controllers/license.js +0 -269
- package/server/src/controllers/oauth.js +0 -474
- package/server/src/controllers/routing-rules.js +0 -129
- package/server/src/controllers/test.js +0 -301
- package/server/src/destroy.js +0 -27
- package/server/src/index.js +0 -25
- package/server/src/middlewares/index.js +0 -3
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -5
- package/server/src/routes/admin.js +0 -469
- package/server/src/routes/content-api.js +0 -37
- package/server/src/routes/index.js +0 -9
- package/server/src/services/account-manager.js +0 -329
- package/server/src/services/analytics.js +0 -512
- package/server/src/services/email-designer.js +0 -717
- package/server/src/services/email-router.js +0 -1446
- package/server/src/services/index.js +0 -17
- package/server/src/services/license-guard.js +0 -423
- package/server/src/services/oauth.js +0 -515
- package/server/src/services/service.js +0 -7
- package/server/src/utils/encryption.js +0 -81
- package/server/src/utils/logger.js +0 -84
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Email Designer Controller
|
|
3
|
-
* Handles CRUD operations for email templates
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
module.exports = ({ strapi }) => ({
|
|
9
|
-
/**
|
|
10
|
-
* Get all templates
|
|
11
|
-
*/
|
|
12
|
-
async findAll(ctx) {
|
|
13
|
-
try {
|
|
14
|
-
const templates = await strapi
|
|
15
|
-
.plugin('magic-mail')
|
|
16
|
-
.service('email-designer')
|
|
17
|
-
.findAll(ctx.query.filters);
|
|
18
|
-
|
|
19
|
-
return ctx.send({
|
|
20
|
-
success: true,
|
|
21
|
-
data: templates,
|
|
22
|
-
});
|
|
23
|
-
} catch (error) {
|
|
24
|
-
ctx.throw(500, error);
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get template by ID
|
|
30
|
-
*/
|
|
31
|
-
async findOne(ctx) {
|
|
32
|
-
try {
|
|
33
|
-
const { id } = ctx.params;
|
|
34
|
-
const template = await strapi
|
|
35
|
-
.plugin('magic-mail')
|
|
36
|
-
.service('email-designer')
|
|
37
|
-
.findOne(id);
|
|
38
|
-
|
|
39
|
-
if (!template) {
|
|
40
|
-
return ctx.notFound('Template not found');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return ctx.send({
|
|
44
|
-
success: true,
|
|
45
|
-
data: template,
|
|
46
|
-
});
|
|
47
|
-
} catch (error) {
|
|
48
|
-
ctx.throw(500, error);
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Create template
|
|
54
|
-
*/
|
|
55
|
-
async create(ctx) {
|
|
56
|
-
try {
|
|
57
|
-
const template = await strapi
|
|
58
|
-
.plugin('magic-mail')
|
|
59
|
-
.service('email-designer')
|
|
60
|
-
.create(ctx.request.body);
|
|
61
|
-
|
|
62
|
-
return ctx.send({
|
|
63
|
-
success: true,
|
|
64
|
-
data: template,
|
|
65
|
-
});
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error.message.includes('limit reached') || error.message.includes('already exists')) {
|
|
68
|
-
return ctx.badRequest(error.message);
|
|
69
|
-
}
|
|
70
|
-
ctx.throw(500, error);
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Update template
|
|
76
|
-
*/
|
|
77
|
-
async update(ctx) {
|
|
78
|
-
try {
|
|
79
|
-
const { id } = ctx.params;
|
|
80
|
-
const template = await strapi
|
|
81
|
-
.plugin('magic-mail')
|
|
82
|
-
.service('email-designer')
|
|
83
|
-
.update(id, ctx.request.body);
|
|
84
|
-
|
|
85
|
-
return ctx.send({
|
|
86
|
-
success: true,
|
|
87
|
-
data: template,
|
|
88
|
-
});
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (error.message.includes('not found')) {
|
|
91
|
-
return ctx.notFound(error.message);
|
|
92
|
-
}
|
|
93
|
-
ctx.throw(500, error);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Delete template
|
|
99
|
-
*/
|
|
100
|
-
async delete(ctx) {
|
|
101
|
-
try {
|
|
102
|
-
const { id } = ctx.params;
|
|
103
|
-
await strapi.plugin('magic-mail').service('email-designer').delete(id);
|
|
104
|
-
|
|
105
|
-
return ctx.send({
|
|
106
|
-
success: true,
|
|
107
|
-
message: 'Template deleted successfully',
|
|
108
|
-
});
|
|
109
|
-
} catch (error) {
|
|
110
|
-
ctx.throw(500, error);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get template versions
|
|
116
|
-
*/
|
|
117
|
-
async getVersions(ctx) {
|
|
118
|
-
try {
|
|
119
|
-
const { id } = ctx.params;
|
|
120
|
-
const versions = await strapi
|
|
121
|
-
.plugin('magic-mail')
|
|
122
|
-
.service('email-designer')
|
|
123
|
-
.getVersions(id);
|
|
124
|
-
|
|
125
|
-
return ctx.send({
|
|
126
|
-
success: true,
|
|
127
|
-
data: versions,
|
|
128
|
-
});
|
|
129
|
-
} catch (error) {
|
|
130
|
-
ctx.throw(500, error);
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Restore version
|
|
136
|
-
*/
|
|
137
|
-
async restoreVersion(ctx) {
|
|
138
|
-
try {
|
|
139
|
-
const { id, versionId } = ctx.params;
|
|
140
|
-
const template = await strapi
|
|
141
|
-
.plugin('magic-mail')
|
|
142
|
-
.service('email-designer')
|
|
143
|
-
.restoreVersion(id, versionId);
|
|
144
|
-
|
|
145
|
-
return ctx.send({
|
|
146
|
-
success: true,
|
|
147
|
-
data: template,
|
|
148
|
-
});
|
|
149
|
-
} catch (error) {
|
|
150
|
-
if (error.message.includes('not found')) {
|
|
151
|
-
return ctx.notFound(error.message);
|
|
152
|
-
}
|
|
153
|
-
ctx.throw(500, error);
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Delete a single version
|
|
159
|
-
*/
|
|
160
|
-
async deleteVersion(ctx) {
|
|
161
|
-
try {
|
|
162
|
-
const { id, versionId } = ctx.params;
|
|
163
|
-
const result = await strapi
|
|
164
|
-
.plugin('magic-mail')
|
|
165
|
-
.service('email-designer')
|
|
166
|
-
.deleteVersion(id, versionId);
|
|
167
|
-
|
|
168
|
-
return ctx.send({
|
|
169
|
-
success: true,
|
|
170
|
-
data: result,
|
|
171
|
-
});
|
|
172
|
-
} catch (error) {
|
|
173
|
-
if (error.message.includes('not found')) {
|
|
174
|
-
return ctx.notFound(error.message);
|
|
175
|
-
}
|
|
176
|
-
if (error.message.includes('does not belong')) {
|
|
177
|
-
return ctx.badRequest(error.message);
|
|
178
|
-
}
|
|
179
|
-
ctx.throw(500, error);
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Delete all versions for a template
|
|
185
|
-
*/
|
|
186
|
-
async deleteAllVersions(ctx) {
|
|
187
|
-
try {
|
|
188
|
-
const { id } = ctx.params;
|
|
189
|
-
const result = await strapi
|
|
190
|
-
.plugin('magic-mail')
|
|
191
|
-
.service('email-designer')
|
|
192
|
-
.deleteAllVersions(id);
|
|
193
|
-
|
|
194
|
-
return ctx.send({
|
|
195
|
-
success: true,
|
|
196
|
-
data: result,
|
|
197
|
-
});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
if (error.message.includes('not found')) {
|
|
200
|
-
return ctx.notFound(error.message);
|
|
201
|
-
}
|
|
202
|
-
ctx.throw(500, error);
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Render template with data
|
|
208
|
-
*/
|
|
209
|
-
async renderTemplate(ctx) {
|
|
210
|
-
try {
|
|
211
|
-
const { templateReferenceId } = ct
|
|
212
|
-
x.params;
|
|
213
|
-
const { data } = ctx.request.body;
|
|
214
|
-
|
|
215
|
-
const rendered = await strapi
|
|
216
|
-
.plugin('magic-mail')
|
|
217
|
-
.service('email-designer')
|
|
218
|
-
.renderTemplate(parseInt(templateReferenceId), data);
|
|
219
|
-
|
|
220
|
-
return ctx.send({
|
|
221
|
-
success: true,
|
|
222
|
-
data: rendered,
|
|
223
|
-
});
|
|
224
|
-
} catch (error) {
|
|
225
|
-
if (error.message.includes('not found')) {
|
|
226
|
-
return ctx.notFound(error.message);
|
|
227
|
-
}
|
|
228
|
-
ctx.throw(500, error);
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Export templates
|
|
234
|
-
*/
|
|
235
|
-
async exportTemplates(ctx) {
|
|
236
|
-
try {
|
|
237
|
-
const { templateIds } = ctx.request.body;
|
|
238
|
-
const templates = await strapi
|
|
239
|
-
.plugin('magic-mail')
|
|
240
|
-
.service('email-designer')
|
|
241
|
-
.exportTemplates(templateIds || []);
|
|
242
|
-
|
|
243
|
-
return ctx.send({
|
|
244
|
-
success: true,
|
|
245
|
-
data: templates,
|
|
246
|
-
});
|
|
247
|
-
} catch (error) {
|
|
248
|
-
if (error.message.includes('requires')) {
|
|
249
|
-
return ctx.forbidden(error.message);
|
|
250
|
-
}
|
|
251
|
-
ctx.throw(500, error);
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Import templates
|
|
257
|
-
*/
|
|
258
|
-
async importTemplates(ctx) {
|
|
259
|
-
try {
|
|
260
|
-
const { templates } = ctx.request.body;
|
|
261
|
-
|
|
262
|
-
if (!Array.isArray(templates)) {
|
|
263
|
-
return ctx.badRequest('Templates must be an array');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const results = await strapi
|
|
267
|
-
.plugin('magic-mail')
|
|
268
|
-
.service('email-designer')
|
|
269
|
-
.importTemplates(templates);
|
|
270
|
-
|
|
271
|
-
return ctx.send({
|
|
272
|
-
success: true,
|
|
273
|
-
data: results,
|
|
274
|
-
});
|
|
275
|
-
} catch (error) {
|
|
276
|
-
if (error.message.includes('requires')) {
|
|
277
|
-
return ctx.forbidden(error.message);
|
|
278
|
-
}
|
|
279
|
-
ctx.throw(500, error);
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get template statistics
|
|
285
|
-
*/
|
|
286
|
-
async getStats(ctx) {
|
|
287
|
-
try {
|
|
288
|
-
const stats = await strapi.plugin('magic-mail').service('email-designer').getStats();
|
|
289
|
-
|
|
290
|
-
return ctx.send({
|
|
291
|
-
success: true,
|
|
292
|
-
data: stats,
|
|
293
|
-
});
|
|
294
|
-
} catch (error) {
|
|
295
|
-
ctx.throw(500, error);
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Get core email template
|
|
301
|
-
*/
|
|
302
|
-
async getCoreTemplate(ctx) {
|
|
303
|
-
try {
|
|
304
|
-
const { coreEmailType } = ctx.params;
|
|
305
|
-
const template = await strapi
|
|
306
|
-
.plugin('magic-mail')
|
|
307
|
-
.service('email-designer')
|
|
308
|
-
.getCoreTemplate(coreEmailType);
|
|
309
|
-
|
|
310
|
-
return ctx.send({
|
|
311
|
-
success: true,
|
|
312
|
-
data: template,
|
|
313
|
-
});
|
|
314
|
-
} catch (error) {
|
|
315
|
-
ctx.throw(500, error);
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Update core email template
|
|
321
|
-
*/
|
|
322
|
-
async updateCoreTemplate(ctx) {
|
|
323
|
-
try {
|
|
324
|
-
const { coreEmailType } = ctx.params;
|
|
325
|
-
const template = await strapi
|
|
326
|
-
.plugin('magic-mail')
|
|
327
|
-
.service('email-designer')
|
|
328
|
-
.updateCoreTemplate(coreEmailType, ctx.request.body);
|
|
329
|
-
|
|
330
|
-
return ctx.send({
|
|
331
|
-
success: true,
|
|
332
|
-
data: template,
|
|
333
|
-
});
|
|
334
|
-
} catch (error) {
|
|
335
|
-
ctx.throw(500, error);
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Download template as HTML or JSON
|
|
341
|
-
*/
|
|
342
|
-
async download(ctx) {
|
|
343
|
-
try {
|
|
344
|
-
const { id } = ctx.params;
|
|
345
|
-
const { type = 'json' } = ctx.query;
|
|
346
|
-
|
|
347
|
-
// Get the template
|
|
348
|
-
const template = await strapi
|
|
349
|
-
.plugin('magic-mail')
|
|
350
|
-
.service('email-designer')
|
|
351
|
-
.findOne(id);
|
|
352
|
-
|
|
353
|
-
if (!template) {
|
|
354
|
-
return ctx.notFound('Template not found');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let fileContent, fileName;
|
|
358
|
-
|
|
359
|
-
if (type === 'json') {
|
|
360
|
-
// Serve JSON design
|
|
361
|
-
fileContent = JSON.stringify(template.design, null, 2);
|
|
362
|
-
fileName = `template-${id}.json`;
|
|
363
|
-
ctx.set('Content-Type', 'application/json');
|
|
364
|
-
} else if (type === 'html') {
|
|
365
|
-
// Serve HTML
|
|
366
|
-
fileContent = template.bodyHtml;
|
|
367
|
-
fileName = `template-${id}.html`;
|
|
368
|
-
ctx.set('Content-Type', 'text/html');
|
|
369
|
-
} else {
|
|
370
|
-
return ctx.badRequest('Invalid type, must be either "json" or "html".');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Set the content disposition to prompt a file download
|
|
374
|
-
ctx.set('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
375
|
-
ctx.send(fileContent);
|
|
376
|
-
} catch (error) {
|
|
377
|
-
strapi.log.error('[magic-mail] Error downloading template:', error);
|
|
378
|
-
ctx.throw(500, error);
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Duplicate template
|
|
384
|
-
*/
|
|
385
|
-
async duplicate(ctx) {
|
|
386
|
-
try {
|
|
387
|
-
const { id } = ctx.params;
|
|
388
|
-
|
|
389
|
-
const duplicated = await strapi
|
|
390
|
-
.plugin('magic-mail')
|
|
391
|
-
.service('email-designer')
|
|
392
|
-
.duplicate(id);
|
|
393
|
-
|
|
394
|
-
return ctx.send({
|
|
395
|
-
success: true,
|
|
396
|
-
data: duplicated,
|
|
397
|
-
});
|
|
398
|
-
} catch (error) {
|
|
399
|
-
if (error.message.includes('not found')) {
|
|
400
|
-
return ctx.notFound(error.message);
|
|
401
|
-
}
|
|
402
|
-
ctx.throw(500, error);
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Send test email for template
|
|
408
|
-
*/
|
|
409
|
-
async testSend(ctx) {
|
|
410
|
-
try {
|
|
411
|
-
const { id } = ctx.params;
|
|
412
|
-
const { to, accountName } = ctx.request.body;
|
|
413
|
-
|
|
414
|
-
// Validate required fields
|
|
415
|
-
if (!to) {
|
|
416
|
-
return ctx.badRequest('Recipient email (to) is required');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Get template
|
|
420
|
-
const template = await strapi
|
|
421
|
-
.plugin('magic-mail')
|
|
422
|
-
.service('email-designer')
|
|
423
|
-
.findOne(id);
|
|
424
|
-
|
|
425
|
-
if (!template) {
|
|
426
|
-
return ctx.notFound('Template not found');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Render template with empty test data (you can add default test data if needed)
|
|
430
|
-
const rendered = await strapi
|
|
431
|
-
.plugin('magic-mail')
|
|
432
|
-
.service('email-designer')
|
|
433
|
-
.renderTemplate(template.templateReferenceId, {
|
|
434
|
-
name: 'Test User',
|
|
435
|
-
email: to,
|
|
436
|
-
// Add more default test variables as needed
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// Send email using the email router service
|
|
440
|
-
const emailRouterService = strapi.plugin('magic-mail').service('email-router');
|
|
441
|
-
|
|
442
|
-
const sendOptions = {
|
|
443
|
-
to,
|
|
444
|
-
subject: rendered.subject || template.subject,
|
|
445
|
-
html: rendered.html,
|
|
446
|
-
text: rendered.text,
|
|
447
|
-
// Add template tracking info
|
|
448
|
-
templateId: template.templateReferenceId,
|
|
449
|
-
templateName: template.name,
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
// If accountName is specified, include it
|
|
453
|
-
if (accountName) {
|
|
454
|
-
sendOptions.accountName = accountName;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const result = await emailRouterService.send(sendOptions);
|
|
458
|
-
|
|
459
|
-
return ctx.send({
|
|
460
|
-
success: true,
|
|
461
|
-
message: 'Test email sent successfully',
|
|
462
|
-
data: {
|
|
463
|
-
recipient: to,
|
|
464
|
-
template: template.name,
|
|
465
|
-
result,
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
} catch (error) {
|
|
469
|
-
strapi.log.error('[magic-mail] Error sending test email:', error);
|
|
470
|
-
return ctx.badRequest(error.message || 'Failed to send test email');
|
|
471
|
-
}
|
|
472
|
-
},
|
|
473
|
-
});
|
|
474
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const controller = require('./controller');
|
|
4
|
-
const accounts = require('./accounts');
|
|
5
|
-
const oauth = require('./oauth');
|
|
6
|
-
const routingRules = require('./routing-rules');
|
|
7
|
-
const license = require('./license');
|
|
8
|
-
const emailDesigner = require('./email-designer');
|
|
9
|
-
const analytics = require('./analytics');
|
|
10
|
-
const test = require('./test');
|
|
11
|
-
|
|
12
|
-
module.exports = {
|
|
13
|
-
controller,
|
|
14
|
-
accounts,
|
|
15
|
-
oauth,
|
|
16
|
-
routingRules,
|
|
17
|
-
license,
|
|
18
|
-
emailDesigner,
|
|
19
|
-
analytics,
|
|
20
|
-
test,
|
|
21
|
-
};
|