@adobe/spacecat-shared-tokowaka-client 1.21.0 → 1.21.1
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/index.js +66 -37
- package/src/utils/suggestion-utils.js +55 -0
- package/test/index.test.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-tokowaka-client-v1.21.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.21.0...@adobe/spacecat-shared-tokowaka-client-v1.21.1) (2026-07-15)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* utility for marking suggestions as covered ([#1810](https://github.com/adobe/spacecat-shared/issues/1810)) ([2709d47](https://github.com/adobe/spacecat-shared/commit/2709d4741bfe2968dfb481ceedf3e283525f5e71))
|
|
6
|
+
|
|
1
7
|
## [@adobe/spacecat-shared-tokowaka-client-v1.21.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.20.0...@adobe/spacecat-shared-tokowaka-client-v1.21.0) (2026-07-14)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
} from './utils/s3-utils.js';
|
|
28
28
|
import {
|
|
29
29
|
omitKeys,
|
|
30
|
-
isEdgeDeployableSuggestionStatus,
|
|
31
30
|
isPatternSuggestion,
|
|
32
31
|
groupSuggestionsByUrlPath,
|
|
33
32
|
filterEligibleSuggestions,
|
|
@@ -36,8 +35,8 @@ import {
|
|
|
36
35
|
cleanupCoveredSuggestions,
|
|
37
36
|
classifySuggestions,
|
|
38
37
|
filterBatchCoveredSuggestions,
|
|
38
|
+
findCoveredSuggestions,
|
|
39
39
|
} from './utils/suggestion-utils.js';
|
|
40
|
-
import { buildUrlMatcher } from './utils/pattern-utils.js';
|
|
41
40
|
import { getEffectiveBaseURL } from './utils/site-utils.js';
|
|
42
41
|
import { removePatternFromMetaconfig, addPatternsToMetaconfig } from './utils/metaconfig-utils.js';
|
|
43
42
|
import { fetchHtmlWithWarmup, calculateForwardedHost } from './utils/custom-html-utils.js';
|
|
@@ -1559,41 +1558,13 @@ class TokowakaClient {
|
|
|
1559
1558
|
suggestion.setUpdatedBy(updatedBy);
|
|
1560
1559
|
// suggestion.save() is deferred — caller batches saves via saveSuggestions.
|
|
1561
1560
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
return m ? [m] : [];
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
if (matchers.length === 0) {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const covered = allSuggestions.filter((s) => {
|
|
1577
|
-
if (s.getId() === suggestion.getId()) {
|
|
1578
|
-
return false;
|
|
1579
|
-
}
|
|
1580
|
-
if (skippedInBatchIds.has(s.getId())) {
|
|
1581
|
-
return false;
|
|
1582
|
-
}
|
|
1583
|
-
if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
|
|
1584
|
-
return false;
|
|
1585
|
-
}
|
|
1586
|
-
if (s.getData()?.edgeDeployed) {
|
|
1587
|
-
return false;
|
|
1588
|
-
}
|
|
1589
|
-
// Path-level pattern suggestions (not domain-wide) are fully covered by a
|
|
1590
|
-
// domain-wide deployment. Other pattern suggestions (including other DW ones) are not.
|
|
1591
|
-
if (isPatternSuggestion(s)) {
|
|
1592
|
-
return coverageField === 'coveredByDomainWide' && !s.getData()?.isDomainWide;
|
|
1593
|
-
}
|
|
1594
|
-
const url = s.getData()?.url;
|
|
1595
|
-
return url && matchers.some((match) => match(url));
|
|
1596
|
-
});
|
|
1561
|
+
const covered = findCoveredSuggestions(
|
|
1562
|
+
suggestion,
|
|
1563
|
+
allowedRegexPatterns,
|
|
1564
|
+
allSuggestions,
|
|
1565
|
+
skippedInBatchIds,
|
|
1566
|
+
this.log,
|
|
1567
|
+
);
|
|
1597
1568
|
|
|
1598
1569
|
// eslint-disable-next-line max-len
|
|
1599
1570
|
this.log.info(`[edge-deploy] Pattern ${suggestion.getId()}: found ${covered.length} coverable per-URL suggestions (field=${coverageField})`);
|
|
@@ -1759,6 +1730,64 @@ class TokowakaClient {
|
|
|
1759
1730
|
return { succeededSuggestions, failedSuggestions, coveredSuggestions };
|
|
1760
1731
|
}
|
|
1761
1732
|
|
|
1733
|
+
/**
|
|
1734
|
+
* Finds and marks the suggestions that fall within a pattern suggestion's
|
|
1735
|
+
* scope (domain-wide or segment/path) as covered and saves them.
|
|
1736
|
+
*
|
|
1737
|
+
* @param {Object} patternSuggestion - The domain-wide / segment pattern suggestion
|
|
1738
|
+
* @param {Array} allSuggestions - Full opportunity suggestion list to search for matches
|
|
1739
|
+
* @param {string} siteId - Site ID
|
|
1740
|
+
* @param {string} [updatedBy]
|
|
1741
|
+
* @returns {Promise<Array>} the marked suggestions
|
|
1742
|
+
*/
|
|
1743
|
+
async markPatternCoveredSuggestions(
|
|
1744
|
+
patternSuggestion,
|
|
1745
|
+
allSuggestions,
|
|
1746
|
+
siteId,
|
|
1747
|
+
updatedBy = 'edge-deploy',
|
|
1748
|
+
) {
|
|
1749
|
+
const allowedRegexPatterns = patternSuggestion.getData()?.allowedRegexPatterns;
|
|
1750
|
+
if (!Array.isArray(allowedRegexPatterns) || allowedRegexPatterns.length === 0) {
|
|
1751
|
+
this.log.warn(`[edge-deploy] Pattern suggestion ${patternSuggestion.getId()} has `
|
|
1752
|
+
+ 'no allowedRegexPatterns, skipping cover-marking');
|
|
1753
|
+
return [];
|
|
1754
|
+
}
|
|
1755
|
+
const covered = findCoveredSuggestions(
|
|
1756
|
+
patternSuggestion,
|
|
1757
|
+
allowedRegexPatterns,
|
|
1758
|
+
allSuggestions,
|
|
1759
|
+
new Set(),
|
|
1760
|
+
this.log,
|
|
1761
|
+
);
|
|
1762
|
+
if (covered.length === 0) {
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
const coverageField = patternSuggestion.getData()?.isDomainWide
|
|
1766
|
+
? 'coveredByDomainWide' : 'coveredByPattern';
|
|
1767
|
+
covered.forEach((s) => {
|
|
1768
|
+
s.setData({ ...s.getData(), [coverageField]: patternSuggestion.getId() });
|
|
1769
|
+
s.setUpdatedBy(updatedBy);
|
|
1770
|
+
});
|
|
1771
|
+
try {
|
|
1772
|
+
await saveSuggestions(this.dataAccess, covered, {
|
|
1773
|
+
sqs: this.sqs,
|
|
1774
|
+
queueUrl: this.importWorkerQueueUrl,
|
|
1775
|
+
siteId,
|
|
1776
|
+
set: { [coverageField]: patternSuggestion.getId() },
|
|
1777
|
+
updatedBy,
|
|
1778
|
+
log: this.log,
|
|
1779
|
+
});
|
|
1780
|
+
this.log.info(`[edge-deploy] Marked ${covered.length} suggestions as ${coverageField}=${patternSuggestion.getId()}`);
|
|
1781
|
+
return covered;
|
|
1782
|
+
} catch (saveError) {
|
|
1783
|
+
this.log.warn(
|
|
1784
|
+
'[edge-deploy] Failed to mark covered suggestions for pattern suggestion '
|
|
1785
|
+
+ `${patternSuggestion.getId()}: ${saveError.message}`,
|
|
1786
|
+
);
|
|
1787
|
+
return [];
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1762
1791
|
/**
|
|
1763
1792
|
* Strips the `applyStale` flag from patches in S3 for the given per-URL suggestions.
|
|
1764
1793
|
* Called when an experiment completes so edge reverts to normal (non-stale) behaviour.
|
|
@@ -284,3 +284,58 @@ export function filterBatchCoveredSuggestions(validSuggestions, patternSuggestio
|
|
|
284
284
|
|
|
285
285
|
return { remaining, skippedInBatch };
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Finds the suggestions in `allSuggestions` that are covered by a pattern suggestion's
|
|
290
|
+
* allowedRegexPatterns
|
|
291
|
+
*
|
|
292
|
+
* @param {Object} patternSuggestion - The domain-wide / segment pattern suggestion
|
|
293
|
+
* @param {Array<string>} allowedRegexPatterns
|
|
294
|
+
* @param {Array} allSuggestions - Full opportunity suggestion list to search
|
|
295
|
+
* @param {Set<string>} [excludeIds] - Suggestion IDs to skip (e.g. already handled in-batch)
|
|
296
|
+
* @param {Object} log - Logger instance
|
|
297
|
+
* @returns {Array} Suggestions covered by the pattern
|
|
298
|
+
*/
|
|
299
|
+
export function findCoveredSuggestions(
|
|
300
|
+
patternSuggestion,
|
|
301
|
+
allowedRegexPatterns,
|
|
302
|
+
allSuggestions,
|
|
303
|
+
excludeIds,
|
|
304
|
+
log,
|
|
305
|
+
) {
|
|
306
|
+
const isDomainWide = patternSuggestion.getData()?.isDomainWide === true;
|
|
307
|
+
const matchers = allowedRegexPatterns.flatMap((p) => {
|
|
308
|
+
const m = buildUrlMatcher(p);
|
|
309
|
+
if (!m) {
|
|
310
|
+
// eslint-disable-next-line max-len
|
|
311
|
+
log.warn(`[edge-deploy] Pattern '${p}' for suggestion ${patternSuggestion.getId()} is invalid, skipping`);
|
|
312
|
+
}
|
|
313
|
+
return m ? [m] : [];
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (matchers.length === 0) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return allSuggestions.filter((s) => {
|
|
321
|
+
if (s.getId() === patternSuggestion.getId()) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (excludeIds?.has(s.getId())) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (!isEdgeDeployableSuggestionStatus(s.getStatus())) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (s.getData()?.edgeDeployed) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
// Path-level pattern suggestions (not domain-wide) are fully covered by a
|
|
334
|
+
// domain-wide deployment. Other pattern suggestions (including other DW ones) are not.
|
|
335
|
+
if (isPatternSuggestion(s)) {
|
|
336
|
+
return isDomainWide && !s.getData()?.isDomainWide;
|
|
337
|
+
}
|
|
338
|
+
const url = s.getData()?.url;
|
|
339
|
+
return url && matchers.some((match) => match(url));
|
|
340
|
+
});
|
|
341
|
+
}
|
package/test/index.test.js
CHANGED
|
@@ -147,6 +147,98 @@ describe('TokowakaClient', () => {
|
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
describe('markPatternCoveredSuggestions', () => {
|
|
151
|
+
const mkPattern = (isDomainWide) => ({
|
|
152
|
+
getId: () => (isDomainWide ? 'dw-1' : 'path-1'),
|
|
153
|
+
getData: () => (isDomainWide
|
|
154
|
+
? { isDomainWide: true, allowedRegexPatterns: ['/*'] }
|
|
155
|
+
: { allowedRegexPatterns: ['/foo/*'] }),
|
|
156
|
+
});
|
|
157
|
+
const mkCovered = (id, url = `https://example.com/foo/${id}`) => ({
|
|
158
|
+
getId: () => id,
|
|
159
|
+
getStatus: () => 'NEW',
|
|
160
|
+
getData: () => ({ url }),
|
|
161
|
+
setData: sinon.stub(),
|
|
162
|
+
setUpdatedBy: sinon.stub(),
|
|
163
|
+
save: sinon.stub().resolves(),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('marks all matching suggestions in the opportunity with coveredByDomainWide', async () => {
|
|
167
|
+
const covered = [mkCovered('a'), mkCovered('b')];
|
|
168
|
+
|
|
169
|
+
const result = await client.markPatternCoveredSuggestions(
|
|
170
|
+
mkPattern(true),
|
|
171
|
+
covered,
|
|
172
|
+
'site-1',
|
|
173
|
+
'tester',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(result).to.deep.equal(covered);
|
|
177
|
+
covered.forEach((s) => {
|
|
178
|
+
expect(s.setData.calledWithMatch({ coveredByDomainWide: 'dw-1' })).to.be.true;
|
|
179
|
+
expect(s.setUpdatedBy.calledOnceWith('tester')).to.be.true;
|
|
180
|
+
});
|
|
181
|
+
expect(client.dataAccess.Suggestion.saveMany.calledOnceWith(covered)).to.be.true;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('marks only URLs matching the segment/path pattern with coveredByPattern', async () => {
|
|
185
|
+
const matching = mkCovered('a', 'https://example.com/foo/a');
|
|
186
|
+
const nonMatching = mkCovered('b', 'https://example.com/bar/b');
|
|
187
|
+
|
|
188
|
+
const result = await client.markPatternCoveredSuggestions(
|
|
189
|
+
mkPattern(false),
|
|
190
|
+
[matching, nonMatching],
|
|
191
|
+
'site-1',
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(result).to.deep.equal([matching]);
|
|
195
|
+
expect(matching.setData.calledWithMatch({ coveredByPattern: 'path-1' })).to.be.true;
|
|
196
|
+
expect(matching.setUpdatedBy.calledOnceWith('edge-deploy')).to.be.true;
|
|
197
|
+
expect(nonMatching.setData.called).to.be.false;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('is a no-op when the pattern suggestion has no allowedRegexPatterns', async () => {
|
|
201
|
+
const noPattern = { getId: () => 'dw-1', getData: () => ({ isDomainWide: true }) };
|
|
202
|
+
|
|
203
|
+
expect(await client.markPatternCoveredSuggestions(noPattern, [mkCovered('a')], 'site-1'))
|
|
204
|
+
.to.deep.equal([]);
|
|
205
|
+
expect(client.dataAccess.Suggestion.saveMany.called).to.be.false;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('is a no-op when nothing in allSuggestions matches', async () => {
|
|
209
|
+
const result = await client.markPatternCoveredSuggestions(mkPattern(true), [], 'site-1');
|
|
210
|
+
|
|
211
|
+
expect(result).to.deep.equal([]);
|
|
212
|
+
expect(client.dataAccess.Suggestion.saveMany.called).to.be.false;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('enqueues a bulk-update job with the passed-in siteId when covered count is large', async () => {
|
|
216
|
+
const covered = Array.from({ length: 1701 }, (_, i) => mkCovered(`c${i}`));
|
|
217
|
+
client.sqs = { sendMessage: sinon.stub().resolves() };
|
|
218
|
+
client.importWorkerQueueUrl = 'https://sqs.test/import-worker-queue';
|
|
219
|
+
|
|
220
|
+
await client.markPatternCoveredSuggestions(mkPattern(true), covered, 'site-1', 'tester');
|
|
221
|
+
|
|
222
|
+
expect(client.sqs.sendMessage).to.have.been.calledOnce;
|
|
223
|
+
const [queueUrl, payload] = client.sqs.sendMessage.firstCall.args;
|
|
224
|
+
expect(queueUrl).to.equal('https://sqs.test/import-worker-queue');
|
|
225
|
+
expect(payload).to.include({ type: 'suggestion-bulk-update', siteId: 'site-1' });
|
|
226
|
+
expect(payload.set).to.deep.equal({ coveredByDomainWide: 'dw-1' });
|
|
227
|
+
expect(payload.updatedBy).to.equal('tester');
|
|
228
|
+
expect(client.dataAccess.Suggestion.saveMany.called).to.be.false;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('warns and returns [] instead of throwing when saveSuggestions fails', async () => {
|
|
232
|
+
client.dataAccess.Suggestion.saveMany.rejects(new Error('DB down'));
|
|
233
|
+
const covered = [mkCovered('a')];
|
|
234
|
+
|
|
235
|
+
const result = await client.markPatternCoveredSuggestions(mkPattern(true), covered, 'site-1');
|
|
236
|
+
|
|
237
|
+
expect(result).to.deep.equal([]);
|
|
238
|
+
expect(client.log.warn.calledWithMatch(/Failed to mark covered suggestions/)).to.be.true;
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
150
242
|
describe('createFrom', () => {
|
|
151
243
|
it('should create client from context', () => {
|
|
152
244
|
const context = {
|