Skip to content

Commit ad6c8f7

Browse files
committed
Merge branch 'main' into 40623-v2
2 parents 3fc04b4 + 8b6f098 commit ad6c8f7

File tree

15 files changed

+401
-64
lines changed

15 files changed

+401
-64
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# CWV Trends Audit — spacecat-shared Changes
2+
3+
## Overview
4+
5+
Adds the `CWV_TRENDS_AUDIT` audit type to `Audit.AUDIT_TYPES` in the `spacecat-shared-data-access` package, enabling the new CWV Trends Audit feature across the SpaceCat ecosystem.
6+
7+
## Change
8+
9+
**File:** `packages/spacecat-shared-data-access/src/models/audit/audit.model.js`
10+
11+
```javascript
12+
static AUDIT_TYPES = {
13+
// ... existing types ...
14+
CWV_TRENDS_AUDIT: 'cwv-trends-audit',
15+
};
16+
```
17+
18+
## Context
19+
20+
The CWV Trends Audit is a weekly audit (`every-sunday`) that:
21+
22+
- Reads 28 days of pre-imported CWV data from S3 (`metrics/cwv-trends/cwv-trends-daily-{date}.json`)
23+
- Classifies URLs as Good / Needs Improvement / Poor using standard CWV thresholds
24+
- Determines device type (mobile/desktop) from site handler configuration
25+
- Creates/updates device-specific opportunities: "Mobile Web Performance Trends Report" or "Desktop Web Performance Trends Report"
26+
- Syncs per-URL suggestions with `CONTENT_UPDATE` type
27+
28+
## Dependent Repositories
29+
30+
| Repository | Dependency | Action Required |
31+
|---|---|---|
32+
| `spacecat-audit-worker` | `@adobe/spacecat-shared-data-access` | Bump version after this package is published |
33+
| `spacecat-api-service` | `@adobe/spacecat-shared-data-access` | Bump version to recognize the new audit type |
34+
35+
## Deployment Order
36+
37+
1. Merge this PR → semantic-release publishes new `@adobe/spacecat-shared-data-access`
38+
2. Bump dependency in `spacecat-audit-worker` and `spacecat-api-service`
39+
3. Register audit: `registerAudit('cwv-trends-audit', false, 'every-sunday', [productCodes])`

packages/spacecat-shared-data-access/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## [@adobe/spacecat-shared-data-access-v3.31.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.30.0...@adobe/spacecat-shared-data-access-v3.31.0) (2026-03-25)
2+
3+
### Features
4+
5+
* Added cwv-trends-audit import type ([#1434](https://github.com/adobe/spacecat-shared/issues/1434)) ([eab9653](https://github.com/adobe/spacecat-shared/commit/eab9653a367208f0cf9691fbf186ffabb9307fea))
6+
7+
## [@adobe/spacecat-shared-data-access-v3.30.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.29.0...@adobe/spacecat-shared-data-access-v3.30.0) (2026-03-25)
8+
9+
### Features
10+
11+
* **data-access:** add allByEnrollmentProductCode to SiteCollection ([#1455](https://github.com/adobe/spacecat-shared/issues/1455)) ([0225791](https://github.com/adobe/spacecat-shared/commit/02257915bbf5aa2e15e1320bbcdfa27b236c44b8))
12+
113
## [@adobe/spacecat-shared-data-access-v3.29.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.28.0...@adobe/spacecat-shared-data-access-v3.29.0) (2026-03-23)
214

315
### Features

packages/spacecat-shared-data-access/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@adobe/spacecat-shared-data-access",
3-
"version": "3.29.0",
3+
"version": "3.31.0",
44
"description": "Shared modules of the Spacecat Services - Data Access",
55
"type": "module",
66
"engines": {

packages/spacecat-shared-data-access/src/models/audit/audit.model.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class Audit extends BaseModel {
9191
COMMERCE_PRODUCT_ENRICHMENTS_YEARLY: 'commerce-product-enrichments-yearly',
9292
COMMERCE_PRODUCT_PAGE_ENRICHMENT: 'commerce-product-page-enrichment',
9393
COMMERCE_PRODUCT_CATALOG_ENRICHMENT: 'commerce-product-catalog-enrichment',
94+
CWV_TRENDS_AUDIT: 'cwv-trends-audit',
9495
OFFSITE_BRAND_PRESENCE: 'offsite-brand-presence',
9596
};
9697

packages/spacecat-shared-data-access/src/models/site-enrollment/site-enrollment.collection.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import BaseCollection from '../base/base.collection.js';
14+
import DataAccessError from '../../errors/data-access.error.js';
1415

1516
/**
1617
* SiteEnrollmentCollection - A class representing a collection of SiteEnrollment entities.
@@ -22,6 +23,30 @@ import BaseCollection from '../base/base.collection.js';
2223
class SiteEnrollmentCollection extends BaseCollection {
2324
static COLLECTION_NAME = 'SiteEnrollmentCollection';
2425

26+
/**
27+
* Returns all site IDs enrolled in a given product code in a single JOIN query.
28+
*
29+
* @param {string} productCode - Product code to filter by (e.g. 'LLMO').
30+
* @returns {Promise<string[]>} Array of siteId strings.
31+
*/
32+
async allSiteIdsByProductCode(productCode) {
33+
if (!productCode) {
34+
throw new DataAccessError('productCode is required', { entityName: 'SiteEnrollment', tableName: 'site_enrollments' });
35+
}
36+
37+
const { data, error } = await this.postgrestService
38+
.from(this.tableName)
39+
.select('site_id, entitlements!inner(product_code)')
40+
.eq('entitlements.product_code', productCode);
41+
42+
if (error) {
43+
this.log.error(`[SiteEnrollmentCollection] Failed to query site_enrollments by productCode - ${error.message}`, error);
44+
throw new DataAccessError('Failed to query site_enrollments by productCode', { entityName: 'SiteEnrollment', tableName: 'site_enrollments' }, error);
45+
}
46+
47+
return (data || []).map((row) => row.site_id);
48+
}
49+
2550
async create(item, options = {}) {
2651
if (item?.siteId && item?.entitlementId) {
2752
const existing = await this.findByIndexKeys({

packages/spacecat-shared-data-access/src/models/site/site.collection.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,35 @@ class SiteCollection extends BaseCollection {
145145
return this.allByProjectId(projectId);
146146
}
147147

148+
/**
149+
* Returns all sites enrolled in a given product (e.g. 'LLMO', 'ASO').
150+
* Uses entityRegistry to chain through EntitlementCollection and SiteEnrollmentCollection,
151+
* then batch-fetches full Site objects.
152+
*
153+
* @param {string} productCode - Product code to filter by (e.g. 'LLMO').
154+
* @returns {Promise<Site[]>}
155+
*/
156+
async allByEnrollmentProductCode(productCode, options = {}) {
157+
if (!hasText(productCode)) {
158+
throw new DataAccessError('productCode is required', this);
159+
}
160+
161+
const siteEnrollmentCollection = this.entityRegistry.getCollection('SiteEnrollmentCollection');
162+
163+
// Query 1: get all site IDs enrolled in the given product (single JOIN query)
164+
const siteIds = await siteEnrollmentCollection.allSiteIdsByProductCode(productCode);
165+
if (siteIds.length === 0) {
166+
return [];
167+
}
168+
169+
// Query 2: batch-fetch Site objects (caller controls which fields to fetch)
170+
const { data: sites } = await this.batchGetByKeys(
171+
siteIds.map((siteId) => ({ siteId })),
172+
options,
173+
);
174+
return sites;
175+
}
176+
148177
async allByOrganizationIdAndProjectName(organizationId, projectName) {
149178
if (!hasText(organizationId)) {
150179
throw new DataAccessError('organizationId is required', this);

packages/spacecat-shared-data-access/test/unit/models/audit/audit.model.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ describe('AuditModel', () => {
218218
COMMERCE_PRODUCT_ENRICHMENTS_YEARLY: 'commerce-product-enrichments-yearly',
219219
COMMERCE_PRODUCT_PAGE_ENRICHMENT: 'commerce-product-page-enrichment',
220220
COMMERCE_PRODUCT_CATALOG_ENRICHMENT: 'commerce-product-catalog-enrichment',
221+
CWV_TRENDS_AUDIT: 'cwv-trends-audit',
221222
OFFSITE_BRAND_PRESENCE: 'offsite-brand-presence',
222223
};
223224

packages/spacecat-shared-data-access/test/unit/models/site-enrollment/site-enrollment.collection.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,73 @@ describe('SiteEnrollmentCollection', () => {
9595
expect(superCreateStub).to.have.been.calledOnceWithExactly(mockRecord, { upsert: true });
9696
});
9797
});
98+
99+
describe('allSiteIdsByProductCode', () => {
100+
let fromStub;
101+
let selectStub;
102+
let eqStub;
103+
104+
beforeEach(() => {
105+
eqStub = sinon.stub();
106+
selectStub = sinon.stub().returns({ eq: eqStub });
107+
fromStub = sinon.stub().returns({ select: selectStub });
108+
instance.postgrestService.from = fromStub;
109+
});
110+
111+
afterEach(() => {
112+
sinon.restore();
113+
});
114+
115+
it('throws DataAccessError when productCode is falsy', async () => {
116+
await expect(instance.allSiteIdsByProductCode(null)).to.be.rejectedWith('productCode is required');
117+
await expect(instance.allSiteIdsByProductCode(undefined)).to.be.rejectedWith('productCode is required');
118+
await expect(instance.allSiteIdsByProductCode('')).to.be.rejectedWith('productCode is required');
119+
});
120+
121+
it('returns array of site IDs for a matching product code', async () => {
122+
eqStub.resolves({
123+
data: [
124+
{ site_id: 'cfa88998-a0a0-4136-b21d-0ff2aa127443' },
125+
{ site_id: 'd1e2f3a4-b5c6-7890-abcd-ef1234567890' },
126+
],
127+
error: null,
128+
});
129+
130+
const result = await instance.allSiteIdsByProductCode('LLMO');
131+
132+
expect(result).to.deep.equal([
133+
'cfa88998-a0a0-4136-b21d-0ff2aa127443',
134+
'd1e2f3a4-b5c6-7890-abcd-ef1234567890',
135+
]);
136+
expect(fromStub).to.have.been.calledOnceWithExactly('site_enrollments');
137+
expect(selectStub).to.have.been.calledOnceWithExactly('site_id, entitlements!inner(product_code)');
138+
expect(eqStub).to.have.been.calledOnceWithExactly('entitlements.product_code', 'LLMO');
139+
});
140+
141+
it('returns empty array when no enrollments match', async () => {
142+
eqStub.resolves({ data: [], error: null });
143+
144+
const result = await instance.allSiteIdsByProductCode('LLMO');
145+
146+
expect(result).to.deep.equal([]);
147+
});
148+
149+
it('returns empty array when data is null', async () => {
150+
eqStub.resolves({ data: null, error: null });
151+
152+
const result = await instance.allSiteIdsByProductCode('LLMO');
153+
154+
expect(result).to.deep.equal([]);
155+
});
156+
157+
it('logs error and throws DataAccessError when query fails', async () => {
158+
const dbError = new Error('DB connection failed');
159+
eqStub.resolves({ data: null, error: dbError });
160+
161+
await expect(instance.allSiteIdsByProductCode('LLMO'))
162+
.to.be.rejectedWith('Failed to query site_enrollments by productCode');
163+
164+
expect(mockLogger.error).to.have.been.called;
165+
});
166+
});
98167
});

packages/spacecat-shared-data-access/test/unit/models/site/site.collection.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,68 @@ describe('SiteCollection', () => {
393393
});
394394
});
395395
});
396+
397+
describe('allByEnrollmentProductCode', () => {
398+
let mockSiteEnrollmentCollection;
399+
400+
beforeEach(() => {
401+
mockSiteEnrollmentCollection = {
402+
allSiteIdsByProductCode: stub(),
403+
};
404+
mockEntityRegistry.getCollection = stub()
405+
.withArgs('SiteEnrollmentCollection')
406+
.returns(mockSiteEnrollmentCollection);
407+
});
408+
409+
it('throws DataAccessError when productCode is falsy', async () => {
410+
await expect(instance.allByEnrollmentProductCode('')).to.be.rejectedWith('productCode is required');
411+
await expect(instance.allByEnrollmentProductCode(null)).to.be.rejectedWith('productCode is required');
412+
await expect(instance.allByEnrollmentProductCode(undefined)).to.be.rejectedWith('productCode is required');
413+
});
414+
415+
it('returns empty array and does not call batchGetByKeys when no site IDs found', async () => {
416+
mockSiteEnrollmentCollection.allSiteIdsByProductCode.resolves([]);
417+
instance.batchGetByKeys = stub();
418+
419+
const result = await instance.allByEnrollmentProductCode('LLMO');
420+
421+
expect(result).to.deep.equal([]);
422+
expect(mockSiteEnrollmentCollection.allSiteIdsByProductCode).to.have.been.calledOnceWithExactly('LLMO');
423+
expect(instance.batchGetByKeys).to.not.have.been.called;
424+
});
425+
426+
it('returns sites fetched by batchGetByKeys with default empty options', async () => {
427+
const siteIds = ['cfa88998-a0a0-4136-b21d-0ff2aa127443', 'd1e2f3a4-b5c6-7890-abcd-ef1234567890'];
428+
const mockSites = [{ getId: () => siteIds[0] }, { getId: () => siteIds[1] }];
429+
mockSiteEnrollmentCollection.allSiteIdsByProductCode.resolves(siteIds);
430+
instance.batchGetByKeys = stub().resolves({ data: mockSites });
431+
432+
const result = await instance.allByEnrollmentProductCode('LLMO');
433+
434+
expect(result).to.deep.equal(mockSites);
435+
expect(instance.batchGetByKeys).to.have.been.calledOnceWithExactly(
436+
[
437+
{ siteId: 'cfa88998-a0a0-4136-b21d-0ff2aa127443' },
438+
{ siteId: 'd1e2f3a4-b5c6-7890-abcd-ef1234567890' },
439+
],
440+
{},
441+
);
442+
});
443+
444+
it('passes caller-supplied options through to batchGetByKeys', async () => {
445+
const siteIds = ['cfa88998-a0a0-4136-b21d-0ff2aa127443'];
446+
const mockSites = [{ getId: () => siteIds[0] }];
447+
const options = { attributes: ['siteId', 'baseURL', 'config'] };
448+
mockSiteEnrollmentCollection.allSiteIdsByProductCode.resolves(siteIds);
449+
instance.batchGetByKeys = stub().resolves({ data: mockSites });
450+
451+
const result = await instance.allByEnrollmentProductCode('LLMO', options);
452+
453+
expect(result).to.deep.equal(mockSites);
454+
expect(instance.batchGetByKeys).to.have.been.calledOnceWithExactly(
455+
[{ siteId: 'cfa88998-a0a0-4136-b21d-0ff2aa127443' }],
456+
options,
457+
);
458+
});
459+
});
396460
});

packages/spacecat-shared-tokowaka-client/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [@adobe/spacecat-shared-tokowaka-client-v1.12.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.12.1...@adobe/spacecat-shared-tokowaka-client-v1.12.2) (2026-03-25)
2+
3+
### Bug Fixes
4+
5+
* optimize preview api ([#1412](https://github.com/adobe/spacecat-shared/issues/1412)) ([3efb9de](https://github.com/adobe/spacecat-shared/commit/3efb9deb1ee6ad962b5288ef124f96fbfbb6e4ae))
6+
17
## [@adobe/spacecat-shared-tokowaka-client-v1.12.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.12.0...@adobe/spacecat-shared-tokowaka-client-v1.12.1) (2026-03-21)
28

39
### Bug Fixes

0 commit comments

Comments
 (0)